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

231 lines
8.6 KiB
JavaScript

/**
* sotc-clash-arrow | Hooks.js
*
* Enregistrement de tous les hooks FoundryVTT :
* - canvasReady : initialisation du layer + interactions
* - canvasTearDown : nettoyage
* - updateCombat : reset des flèches au début d'un nouveau round
* - deleteCombat : reset à la fin du combat
* - renderSceneControls (optionnel) : bouton reset pour le GM
*/
import { ClashArrowLayer } from "./ClashArrowLayer.js";
import { ArrowStore } from "./ArrowStore.js";
import { initDieInteraction } from "./DieInteraction.js";
import { hookSIOMarkers, getSIODieCenter } from "./SIOBridge.js";
const SBSA_MODULE_ID = "sotc-battle-show-ability";
// ── Setting : mode debug (GM uniquement) ─────────────────────────────────────
export function registerSettings() {
game.settings.register("sotc-clash-arrow", "debugForceState", {
name: game.i18n.localize("SOTCCA.Settings.DebugForceState.Name"),
hint: game.i18n.localize("SOTCCA.Settings.DebugForceState.Hint"),
scope: "world",
config: true,
type: Boolean,
default: false,
restricted: true,
requiresReload: false,
});
}
export function registerHooks() {
// ── Initialisation globale (une seule fois) ────────────────────────────
Hooks.once("ready", () => {
ArrowStore.initSocket();
initDieInteraction();
hookSIOMarkers();
});
// ── Canvas teardown ────────────────────────────────────────────────────
let _currentSceneId = null;
Hooks.on("canvasReady", () => {
const newSceneId = canvas.scene?.id;
if (_currentSceneId && _currentSceneId !== newSceneId) {
// Vrai changement de scène → vider le store
ArrowStore.clear();
}
_currentSceneId = newSceneId;
ClashArrowLayer.init();
// Restaurer les flèches persistées (survit au F5)
// Léger délai pour laisser SIO finir d'initialiser ses marqueurs PIXI
setTimeout(() => {
if (game.user.isGM) {
// Le GM recharge depuis les flags de scène (source de vérité)
ArrowStore.loadFromScene(getSIODieCenter);
} else {
// Les joueurs demandent au GM de leur envoyer l'état courant
ArrowStore.requestSync();
}
}, 200);
});
Hooks.on("canvasTearDown", () => {
// Ne détruire les Graphics que si la scène change (géré dans canvasReady)
// On ne touche pas au store ici pour préserver les flèches lors des refreshs
ClashArrowLayer.tearDown();
});
// ── Reset au nouveau round ─────────────────────────────────────────────
// Aligné sur SBSA : combatRound, GM uniquement
Hooks.on("combatRound", (combat, _roundData, _options) => {
if (!game.user.isGM) return;
ArrowStore.clear();
});
Hooks.on("deleteCombat", () => {
ArrowStore.clear();
});
// ── Suppression des flèches quand un dé passe en Used/Unused ─────────────
Hooks.on("updateCombatant", (combatant, changes) => {
// On ne réagit que si dieState a changé
const newState = foundry.utils.getProperty(changes, `flags.${SBSA_MODULE_ID}.dieState`);
if (newState === undefined) return;
// On ne réagit pas si le dé est dans l'état Applied Skill (3)
if (newState === 3) return;
// Retrouver le token et l'index du dé via le même tri que _getCombatantForDie
const tokenId = combatant.tokenId;
const tokenDoc = canvas.scene?.tokens?.get(tokenId);
if (!tokenDoc) return;
const sortedCombatants = game.combat.combatants
.filter(c => c.tokenId === tokenId)
.sort((a, b) => (b.initiative ?? -Infinity) - (a.initiative ?? -Infinity));
const dieIndex = sortedCombatants.indexOf(combatant);
if (dieIndex === -1) return;
// dieState 1 (Used) → marquer les flèches sortantes en marron
// dieState 0 (Unused) → supprimer les flèches sortantes
// dieState 2 (Saved Defense) → supprimer les flèches sortantes (on pourra en retracer 1)
const isUsed = newState === 1;
for (const arrow of ArrowStore.all()) {
if (arrow.fromToken === tokenId && arrow.fromDie === dieIndex) {
if (isUsed) {
ArrowStore.markUsed(arrow.id);
} else {
ArrowStore.removeArrow(arrow.id);
}
}
}
});
// ── Bouton de reset manuel (GM uniquement) ─────────────────────────────
Hooks.on("getSceneControlButtons", (controls) => {
if (!game.user.isGM) return;
// Ajouter au groupe "token" (première barre)
const tokenControls = controls.find(c => c.name === "token");
if (!tokenControls) return;
tokenControls.tools.push({
name : "sotcca-reset",
title : game.i18n.localize("SOTCCA.ArrowsReset"),
icon : "fas fa-times-circle",
button : true,
onClick: () => {
ArrowStore.clear();
ui.notifications.info(game.i18n.localize("SOTCCA.ArrowsReset"));
},
});
});
// ── Bouton debug : forcer l'état du dé (visible seulement si l'option est activée) ──
Hooks.on("renderApplication", (app, html) => {
// Vérifier que l'option debug est activée
if (!game.settings.get("sotc-clash-arrow", "debugForceState")) return;
// On cible uniquement le panneau SBSA
if (!html[0]?.querySelector(".sotc-bsa-panel")) return;
const panel = html[0].querySelector(".sotc-bsa-panel");
const combatantId = panel?.dataset?.combatantId;
if (!combatantId) return;
const combatant = game.combat?.combatants.get(combatantId);
if (!combatant) return;
// Afficher sur tous les états (pas seulement quand assigné)
// → le bloc debug permet de forcer l'état peu importe la situation
// Injecter le bloc debug après la section d'état
const stateSection = html[0].querySelector(".bsa-state-section");
if (!stateSection) return;
const debugHtml = `
<div class="sotcca-debug-section" style="
margin-top: 6px;
padding: 4px 6px;
border: 1px dashed #888;
border-radius: 4px;
opacity: 0.75;
">
<div style="font-size: 10px; color: #aaa; margin-bottom: 4px;">
🛠 ${game.i18n.localize("SOTCCA.Debug.Title")}
</div>
<div style="display: flex; gap: 4px; flex-wrap: wrap;">
<button class="sotcca-debug-state-btn" data-state="0" style="flex:1; font-size:11px; padding:2px 4px;">
${game.i18n.localize("SOTC_BSA.Panel.StateUnused")}
</button>
<button class="sotcca-debug-state-btn" data-state="1" style="flex:1; font-size:11px; padding:2px 4px;">
${game.i18n.localize("SOTC_BSA.Panel.StateUsed")}
</button>
<button class="sotcca-debug-state-btn" data-state="2" style="flex:1; font-size:11px; padding:2px 4px;">
${game.i18n.localize("SOTC_BSA.Panel.StateSaved")}
</button>
<button class="sotcca-debug-state-btn" data-state="3" style="flex:1; font-size:11px; padding:2px 4px;">
${game.i18n.localize("SOTC_BSA.Panel.StateAssigned")}
</button>
</div>
</div>
`;
stateSection.insertAdjacentHTML("afterend", debugHtml);
// Brancher les listeners sur les nouveaux boutons
html[0].querySelectorAll(".sotcca-debug-state-btn").forEach(btn => {
btn.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
const state = parseInt(btn.dataset.state);
await combatant.setFlag(SBSA_MODULE_ID, "dieState", state);
ui.notifications.info(
game.i18n.format("SOTCCA.Debug.StateForced", { state })
);
});
});
});
ArrowStore.on?.("change", () => {
// Détecter les nouvelles flèches qui viennent de passer en mutuel
// (géré dans ArrowStore.addArrow → on écoute ici pour le message chat)
});
// Hook interne émis par ArrowStore pour les nouveaux clashes mutuels
Hooks.on("sotcca:mutualClash", (arrow) => {
const attacker = canvas.scene.tokens.get(arrow.fromToken)?.name ?? arrow.fromToken;
const defender = canvas.scene.tokens.get(arrow.toToken)?.name ?? arrow.toToken;
ChatMessage.create({
content: `<div class="sotcca-mutual-clash-msg">
⚔️ ${game.i18n.format("SOTCCA.MutualClash", { attacker, defender })}
</div>`,
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
});
});
}