sotc-foundry-suite/sotc-clash-arrow/scripts/SIOBridge.js

228 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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