/** * 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); }