sotc-battle-show-ability : affichage des capacités en combat

This commit is contained in:
Yasser Boulayoune 2026-05-01 16:26:00 +02:00
parent 84aa6b3437
commit e35a3996ee
11 changed files with 1363 additions and 0 deletions

View file

@ -0,0 +1,228 @@
/**
* 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);
}