/** * sotc-damage-wizard | DamageWizardApp.js * * Fenêtre Application FoundryVTT gérant la résolution des dégâts. * Utilise le moteur DamageEngine.resolveClash() qui implémente les * règles officielles SotC (séquence linéaire, counter-dice, PE, etc.) */ import { getAssignedSkill, getSkillDice, resolveDie, resolveClash, applyClashResults, sendResultToChat, } from "./DamageEngine.js"; /** * Cache statique des résultats : persiste entre fermetures/réouvertures. * Clé : attackerCombatantId * Valeur : { atkRolls, defRolls, clashResult } */ export const _rollCache = new Map(); export class DamageWizardApp extends Application { constructor(attackerCombatantId, defenderCombatantId, isClash = false, options = {}) { super(options); this.attackerCombatantId = attackerCombatantId; this.defenderCombatantId = defenderCombatantId; this.isClash = isClash; // Restaurer depuis le cache si déjà rollé const cached = _rollCache.get(attackerCombatantId); this._atkRolls = cached?.atkRolls ?? null; this._defRolls = cached?.defRolls ?? null; this._clashResult = cached?.clashResult ?? null; } /* ------------------------------------------------------------------ */ /* defaultOptions */ /* ------------------------------------------------------------------ */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { id: "sotc-dw-app", template: "modules/sotc-damage-wizard/templates/damage-wizard.hbs", classes: ["sotc-dw-window"], width: 820, height: "auto", resizable: false, popOut: true, }); } /* ------------------------------------------------------------------ */ /* Accesseurs */ /* ------------------------------------------------------------------ */ get attackerCombatant() { return game.combat?.combatants.get(this.attackerCombatantId) ?? null; } get defenderCombatant() { return game.combat?.combatants.get(this.defenderCombatantId) ?? null; } get attackerActor() { return this.attackerCombatant?.actor ?? null; } get defenderActor() { return this.defenderCombatant?.actor ?? null; } /* ------------------------------------------------------------------ */ /* _buildSideData */ /* ------------------------------------------------------------------ */ _buildSideData(combatant) { if (!combatant) return null; const actor = combatant.actor; if (!actor) return null; const sys = actor.system; const skill = getAssignedSkill(combatant); const dice = skill ? getSkillDice(skill) : []; return { combatantId: combatant.id, actorId: actor.id, name: combatant.name, img: actor.img, hpValue: sys.health?.value ?? 0, hpMax: sys.health?.max ?? 1, hpPercent: Math.round(((sys.health?.value ?? 0) / (sys.health?.max ?? 1)) * 100), staggerValue: sys.stagger?.value ?? 0, staggerMax: sys.stagger?.max ?? 1, staggerPercent: Math.round(((sys.stagger?.value ?? 0) / (sys.stagger?.max ?? 1)) * 100), lightValue: sys.light?.value ?? 0, lightMax: sys.light?.max ?? 1, emotion: sys.emotion ?? 0, skillName: skill?.name ?? null, skillImg: skill?.img ?? null, dice, }; } /* ------------------------------------------------------------------ */ /* getData */ /* ------------------------------------------------------------------ */ async getData(options = {}) { const attacker = this._buildSideData(this.attackerCombatant); const defender = this._buildSideData(this.defenderCombatant); const hasRolled = !!this._clashResult; // ── Construire les lignes de matchup pour le template ────────────── let matchupRows = null; let summaryAtk = null; let summaryDef = null; let summaryPE = null; if (this._clashResult) { const r = this._clashResult; matchupRows = r.matchups.map(m => ({ side: m.side, dieType: m.dieType, power: m.power, formulaDisplay: m.formulaDisplay, oppType: m.oppType, oppPower: m.oppPower, oppFormulaDisplay: m.oppFormulaDisplay ?? null, clashType: m.clashType, verdict: m.verdict, effects: m.effects, })); // Résumé dégâts summaryAtk = { name: attacker?.name ?? "?", hpDmg: r.atkHpDamage, stgDmg: r.atkStaggerDamage, stgGain: r.defStaggerRegain, // regain du défenseur (evade win) }; summaryDef = { name: attacker?.name ?? "?", // dégâts SUR l'attaquant hpDmg: r.defHpDamage, stgDmg: r.defStaggerDamage, stgGain: r.atkStaggerRegain, }; summaryPE = { atkPE: r.atkPE, defPE: r.defPE }; } return { isClash: this.isClash, attacker, defender, hasRolled, matchupRows, summaryAtk, summaryDef, summaryPE, clashResult: this._clashResult, }; } /* ------------------------------------------------------------------ */ /* activateListeners */ /* ------------------------------------------------------------------ */ activateListeners(html) { super.activateListeners(html); html.find(".dw-foot-roll").click(() => this._onRoll()); html.find(".dw-foot-apply").click(() => this._onApply()); html.find(".dw-foot-cancel").click(() => this.close()); } /* ------------------------------------------------------------------ */ /* Roll */ /* ------------------------------------------------------------------ */ async _onRoll() { if (this._clashResult) { ui.notifications.warn(game.i18n.localize("SOTC_DW.AlreadyRolled")); return; } const atkActor = this.attackerActor; const defActor = this.defenderActor; if (!atkActor || !defActor) return; const atkSkill = getAssignedSkill(this.attackerCombatant); if (!atkSkill) { ui.notifications.warn(game.i18n.localize("SOTC_DW.NoSkill")); return; } // ── Roll attaquant ───────────────────────────────────────────────── const atkDice = getSkillDice(atkSkill); this._atkRolls = []; for (const die of atkDice) { const res = await resolveDie(die, atkActor); this._atkRolls.push({ die, ...res }); } // ── Roll défenseur (clash uniquement) ────────────────────────────── this._defRolls = []; if (this.isClash) { const defSkill = getAssignedSkill(this.defenderCombatant); if (defSkill) { for (const die of getSkillDice(defSkill)) { const res = await resolveDie(die, defActor); this._defRolls.push({ die, ...res }); } } } // ── Résolution des clashes ───────────────────────────────────────── this._clashResult = resolveClash(this._atkRolls, this._defRolls, defActor, atkActor); // Persister dans le cache _rollCache.set(this.attackerCombatantId, { atkRolls: this._atkRolls, defRolls: this._defRolls, clashResult: this._clashResult, }); this.render(false); } /* ------------------------------------------------------------------ */ /* Apply */ /* ------------------------------------------------------------------ */ async _onApply() { if (!this._clashResult) { ui.notifications.warn(game.i18n.localize("SOTC_DW.RollFirst")); return; } const atkActor = this.attackerActor; const defActor = this.defenderActor; if (!atkActor || !defActor) return; // Appliquer tous les effets (dégâts, PE, états des dés) await applyClashResults( this._clashResult, atkActor, defActor, this.attackerCombatant, this.defenderCombatant ); // Message de chat const atkSkill = getAssignedSkill(this.attackerCombatant); await sendResultToChat({ attackerName: this.attackerCombatant?.name ?? "?", defenderName: this.defenderCombatant?.name ?? "?", skillName: atkSkill?.name ?? "?", result: this._clashResult, }); ui.notifications.info(game.i18n.localize("SOTC_DW.DamageApplied")); // Vider le cache et réinitialiser l'état local _rollCache.delete(this.attackerCombatantId); this._atkRolls = null; this._defRolls = null; this._clashResult = null; this.render(false); } }