sotc-foundry-suite/sotc-damage-wizard/module/DamageWizardApp.js

262 lines
9.1 KiB
JavaScript

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