228 lines
8.3 KiB
JavaScript
228 lines
8.3 KiB
JavaScript
/**
|
||
* sotc-clash-arrow | SIOBridge.js
|
||
*
|
||
* Pont d'intégration avec SIO et SBSA.
|
||
*
|
||
* LEÇON CLUE (apprise du module SBSA) :
|
||
* L'API PIXI events ne fonctionne pas correctement pour ajouter des
|
||
* listeners supplémentaires sur les marqueurs SIO en Foundry v11.
|
||
* La solution fiable est d'écouter les events souris NATIFS sur
|
||
* l'élément HTML #board, exactement comme le fait SBSA.
|
||
*
|
||
* Correspondance index → combatant (identique à SIO) :
|
||
* Les combatants d'un token sont triés par initiative décroissante.
|
||
* marker[i] correspond à combatants[i] dans ce tri.
|
||
*
|
||
* Compétence assignée (flags SBSA réels) :
|
||
* combatant.getFlag("sotc-battle-show-ability", "assignedSkillId")
|
||
* combatant.getFlag("sotc-battle-show-ability", "dieState") → 0/1/2/3
|
||
* dieState === 3 signifie "compétence assignée"
|
||
*/
|
||
|
||
const SIO_FLAG_SCOPE = "stars-initiative-overlay";
|
||
const SBSA_MODULE_ID = "sotc-battle-show-ability";
|
||
const HIT_RADIUS = 28; // pixels écran (getBounds retourne des coords écran sur #board)
|
||
|
||
// ── API publique ──────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Installe le handler mousedown natif sur #board.
|
||
* Appelé une fois depuis Hooks.js au canvasReady.
|
||
*/
|
||
export function hookSIOMarkers() {
|
||
_installBoardHandler();
|
||
// Réinstaller si la scène change
|
||
Hooks.on("canvasReady", () => _installBoardHandler());
|
||
}
|
||
|
||
/**
|
||
* Retourne les coordonnées MONDE (canvas) du centre d'un marqueur SIO.
|
||
* Utilisé pour dessiner les flèches dans ClashArrowLayer.
|
||
* @param {TokenDocument} tokenDoc
|
||
* @param {number} dieIndex
|
||
* @returns {{ x: number, y: number }}
|
||
*/
|
||
export function getSIODieCenter(tokenDoc, dieIndex) {
|
||
const token = tokenDoc.object;
|
||
const marker = token?._initiativeMarkers?.[dieIndex];
|
||
if (marker) {
|
||
const b = marker.getBounds();
|
||
// getBounds() retourne des coordonnées écran (#board) → convertir en coords monde
|
||
const screenCenter = { x: b.x + b.width / 2, y: b.y + b.height / 2 };
|
||
return canvas.stage.toLocal(screenCenter);
|
||
}
|
||
// Fallback : centre du token en coords monde
|
||
return { x: token.center.x, y: token.center.y };
|
||
}
|
||
|
||
/**
|
||
* Cherche quel dé SIO se trouve sous les coordonnées écran (x, y).
|
||
* Retourne { tokenDoc, dieIndex, combatant } ou null.
|
||
* @param {number} screenX – coordonnées relatives à #board
|
||
* @param {number} screenY
|
||
* @returns {{ tokenDoc: TokenDocument, dieIndex: number, combatant: Combatant }|null}
|
||
*/
|
||
export function findDieUnderScreenPoint(screenX, screenY) {
|
||
for (const token of canvas.tokens.placeables) {
|
||
const markers = token._initiativeMarkers;
|
||
if (!markers?.length) continue;
|
||
|
||
for (let i = 0; i < markers.length; i++) {
|
||
const b = markers[i].getBounds();
|
||
const cx = b.x + b.width / 2;
|
||
const cy = b.y + b.height / 2;
|
||
|
||
if (Math.hypot(screenX - cx, screenY - cy) > HIT_RADIUS) continue;
|
||
|
||
const combatant = _getCombatantForDie(token.document, i);
|
||
if (!combatant) continue;
|
||
|
||
return { tokenDoc: token.document, dieIndex: i, combatant };
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Retourne le dieState SBSA du dé `dieIndex` d'un token.
|
||
* @param {TokenDocument} tokenDoc
|
||
* @param {number} dieIndex
|
||
* @returns {number} 0=Unused, 1=Used, 2=Saved Defense, 3=Applied Skill
|
||
*/
|
||
export function getDieState(tokenDoc, dieIndex) {
|
||
const combatant = _getCombatantForDie(tokenDoc, dieIndex);
|
||
if (!combatant) return 0;
|
||
return combatant.getFlag(SBSA_MODULE_ID, "dieState") ?? 0;
|
||
}
|
||
|
||
/**
|
||
* Retourne l'attack_weight de la compétence assignée au dé `dieIndex`.
|
||
* - Si aucune compétence ou pas d'attack_weight → retourne 1 (1 flèche max)
|
||
* - Sinon retourne la valeur numérique de attack_weight
|
||
* @param {TokenDocument} tokenDoc
|
||
* @param {number} dieIndex
|
||
* @returns {number}
|
||
*/
|
||
export function getAttackWeightForDie(tokenDoc, dieIndex) {
|
||
const combatant = _getCombatantForDie(tokenDoc, dieIndex);
|
||
if (!combatant) return 1;
|
||
|
||
const skillId = combatant.getFlag(SBSA_MODULE_ID, "assignedSkillId") ?? null;
|
||
if (!skillId) return 1;
|
||
|
||
const actor = tokenDoc.actor;
|
||
if (!actor) return 1;
|
||
|
||
const skill = actor.items.get(skillId);
|
||
if (!skill) return 1;
|
||
|
||
const weight = skill.system?.weight ?? null;
|
||
if (!weight || weight <= 0) return 1;
|
||
|
||
return weight;
|
||
}
|
||
|
||
/**
|
||
* Retourne true si le dé `dieIndex` d'un token a une compétence assignée.
|
||
* États autorisés à créer une flèche :
|
||
* - dieState === 3 (Applied Skill)
|
||
* - dieState === 2 (Saved Defense Dice) — le dé sauvegardé réutilise sa compétence
|
||
* @param {TokenDocument} tokenDoc
|
||
* @param {number} dieIndex
|
||
* @returns {boolean}
|
||
*/
|
||
export function tokenHasActiveSkillOnDie(tokenDoc, dieIndex) {
|
||
const combatant = _getCombatantForDie(tokenDoc, dieIndex);
|
||
if (!combatant) return false;
|
||
const dieState = combatant.getFlag(SBSA_MODULE_ID, "dieState") ?? 0;
|
||
const assignedId = combatant.getFlag(SBSA_MODULE_ID, "assignedSkillId") ?? null;
|
||
return (dieState === 3 || dieState === 2) && !!assignedId;
|
||
}
|
||
|
||
// ── Helpers internes ──────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Retourne le combatant correspondant à marker[dieIndex] selon le tri SIO.
|
||
* Identique au getter sioMarkerIndex de SpeedDiePanelApp (nouvelle version SBSA) :
|
||
* tri par initiative décroissante, marker[0] = initiative la plus haute.
|
||
* @param {TokenDocument} tokenDoc
|
||
* @param {number} dieIndex
|
||
* @returns {Combatant|null}
|
||
*/
|
||
function _getCombatantForDie(tokenDoc, dieIndex) {
|
||
if (!game.combat) return null;
|
||
const combatants = game.combat.combatants
|
||
.filter(c => c.tokenId === tokenDoc.id)
|
||
.sort((a, b) => (b.initiative ?? -Infinity) - (a.initiative ?? -Infinity));
|
||
return combatants[dieIndex] ?? null;
|
||
}
|
||
|
||
/**
|
||
* Installe (ou réinstalle) les listeners natifs sur #board.
|
||
*/
|
||
function _installBoardHandler() {
|
||
const board = document.getElementById("board");
|
||
if (!board) return;
|
||
|
||
// Nettoyer les anciens handlers
|
||
if (board._sotcca_downHandler) {
|
||
board.removeEventListener("mousedown", board._sotcca_downHandler);
|
||
board.removeEventListener("mouseup", board._sotcca_upHandler);
|
||
board.removeEventListener("mousemove", board._sotcca_moveHandler);
|
||
}
|
||
|
||
board._sotcca_downHandler = _onBoardMouseDown;
|
||
board._sotcca_upHandler = _onBoardMouseUp;
|
||
board._sotcca_moveHandler = _onBoardMouseMove;
|
||
|
||
board.addEventListener("mousedown", board._sotcca_downHandler);
|
||
board.addEventListener("mouseup", board._sotcca_upHandler);
|
||
board.addEventListener("mousemove", board._sotcca_moveHandler, { passive: true });
|
||
|
||
// Bloquer le menu contextuel navigateur sur les dés
|
||
if (!board._sotcca_ctxHandler) {
|
||
board._sotcca_ctxHandler = (e) => {
|
||
const rect = board.getBoundingClientRect();
|
||
const hit = findDieUnderScreenPoint(e.clientX - rect.left, e.clientY - rect.top);
|
||
if (hit) e.preventDefault();
|
||
};
|
||
board.addEventListener("contextmenu", board._sotcca_ctxHandler);
|
||
}
|
||
|
||
console.log("sotc-clash-arrow | Handler mousedown installé sur #board.");
|
||
}
|
||
|
||
// ── Handlers DOM → délèguent via Hooks à DieInteraction ──────────────────────
|
||
|
||
function _onBoardMouseDown(event) {
|
||
const board = document.getElementById("board");
|
||
const rect = board.getBoundingClientRect();
|
||
const sx = event.clientX - rect.left;
|
||
const sy = event.clientY - rect.top;
|
||
|
||
const hit = findDieUnderScreenPoint(sx, sy);
|
||
if (!hit) return;
|
||
|
||
if (event.button === 0) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
Hooks.callAll("sotcca:diePointerDown", hit.tokenDoc, hit.dieIndex, sx, sy);
|
||
} else if (event.button === 2) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
Hooks.callAll("sotcca:dieRightClick", hit.tokenDoc, hit.dieIndex);
|
||
}
|
||
}
|
||
|
||
function _onBoardMouseMove(event) {
|
||
const board = document.getElementById("board");
|
||
const rect = board.getBoundingClientRect();
|
||
Hooks.callAll("sotcca:boardMouseMove", event.clientX - rect.left, event.clientY - rect.top);
|
||
}
|
||
|
||
function _onBoardMouseUp(event) {
|
||
if (event.button !== 0) return;
|
||
const board = document.getElementById("board");
|
||
const rect = board.getBoundingClientRect();
|
||
Hooks.callAll("sotcca:boardMouseUp", event.clientX - rect.left, event.clientY - rect.top);
|
||
}
|