sotc-clash-arrow : flèches de clash entre combattants
This commit is contained in:
commit
ad63cafdc6
10 changed files with 2414 additions and 0 deletions
262
sotc-damage-wizard/module/DamageWizardApp.js
Normal file
262
sotc-damage-wizard/module/DamageWizardApp.js
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue