262 lines
9.1 KiB
JavaScript
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);
|
|
}
|
|
}
|