sotc-damage-wizard : assistant de calcul des dégâts
This commit is contained in:
parent
ad63cafdc6
commit
84aa6b3437
7 changed files with 1532 additions and 0 deletions
401
sotc-battle-show-ability/module/SpeedDiePanelApp.js
Normal file
401
sotc-battle-show-ability/module/SpeedDiePanelApp.js
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
/**
|
||||
* sotc-battle-show-ability — SpeedDiePanelApp
|
||||
* Fenêtre Application FoundryVTT affichant le panneau d'un Dé de Vitesse.
|
||||
*
|
||||
* Architecture SotC :
|
||||
* - 1 token avec N dés de vitesse → N combatants dans le tracker
|
||||
* - Combatant principal : flags.sotc.isSpeedDieClone absent/false
|
||||
* - Clones : flags.sotc.isSpeedDieClone = true, flags.sotc.speedDieIndex = i
|
||||
* - État utilisé : combatant.flags.sotc.used (boolean)
|
||||
* - Compétence assignée : flag module "sotc-battle-show-ability" → "assignedSkillId"
|
||||
*/
|
||||
|
||||
export class SpeedDiePanelApp extends Application {
|
||||
|
||||
constructor(combatantId, options = {}) {
|
||||
super(options);
|
||||
this.combatantId = combatantId;
|
||||
this._activeTab = "skills";
|
||||
this._previewSkillId = null;
|
||||
}
|
||||
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
id: "sotc-bsa-panel",
|
||||
title: game.i18n.localize("SOTC_BSA.Panel.Title"),
|
||||
template: "modules/sotc-battle-show-ability/templates/speed-die-panel.hbs",
|
||||
classes: ["sotc-bsa-window"],
|
||||
width: 320,
|
||||
height: "auto",
|
||||
resizable: false,
|
||||
popOut: true
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
get combatant() {
|
||||
return game.combat?.combatants.get(this.combatantId) ?? null;
|
||||
}
|
||||
|
||||
get actor() {
|
||||
return this.combatant?.actor ?? null;
|
||||
}
|
||||
|
||||
get dieNumber() {
|
||||
// Position visuelle 1-based dans l'ordre SIO (tri par initiative décroissante)
|
||||
return this.sioMarkerIndex + 1;
|
||||
}
|
||||
|
||||
get dieState() {
|
||||
const c = this.combatant;
|
||||
if (!c) return 0;
|
||||
// Si une compétence est assignée, l'état est toujours forcé à 3 (Assigned)
|
||||
if (this.assignedSkillId) return 3;
|
||||
const state = c.getFlag("sotc-battle-show-ability", "dieState");
|
||||
if (state === undefined || state === null) {
|
||||
return c.getFlag("sotc", "used") ? 1 : 0;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
get assignedSkillId() {
|
||||
return this.combatant?.getFlag("sotc-battle-show-ability", "assignedSkillId") ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule l'index du marqueur SIO pour ce combatant.
|
||||
* SIO trie les combatants par initiative décroissante — le combatant avec
|
||||
* la plus haute initiative est à l'index 0 (marqueur le plus à gauche).
|
||||
* speedDieIndex est fixe à la création et ne reflète pas cet ordre.
|
||||
*/
|
||||
get sioMarkerIndex() {
|
||||
const combatant = this.combatant;
|
||||
if (!combatant) return 0;
|
||||
const tokenId = combatant.tokenId;
|
||||
const sorted = game.combat.combatants
|
||||
.filter(c => c.tokenId === tokenId)
|
||||
.sort((a, b) => (b.initiative ?? -Infinity) - (a.initiative ?? -Infinity));
|
||||
const idx = sorted.findIndex(c => c.id === combatant.id);
|
||||
return idx === -1 ? 0 : idx;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* getData */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
async getData(options = {}) {
|
||||
const actor = this.actor;
|
||||
const combatant = this.combatant;
|
||||
|
||||
if (!actor || !combatant) return { error: true };
|
||||
|
||||
const sys = actor.system;
|
||||
|
||||
const hpValue = sys.health?.value ?? 0;
|
||||
const hpMax = sys.health?.max ?? 1;
|
||||
const staggerValue = sys.stagger?.value ?? 0;
|
||||
const staggerMax = sys.stagger?.max ?? 1;
|
||||
const lightValue = sys.light?.value ?? 0;
|
||||
const lightMax = sys.light?.max ?? 1;
|
||||
const emotion = sys.emotion ?? 0;
|
||||
|
||||
const assignedId = this.assignedSkillId;
|
||||
|
||||
const mapItem = (item) => {
|
||||
const lc = item.system.light_cost ?? 0;
|
||||
const ec = item.system.emotion_cost ?? 0;
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
img: item.img,
|
||||
lightCost: lc,
|
||||
emotionCost: ec,
|
||||
canAffordLight: lightValue >= lc,
|
||||
canAffordEmotion: emotion >= ec,
|
||||
selected: item.id === this._previewSkillId,
|
||||
weight: item.system.weight ?? 0
|
||||
};
|
||||
};
|
||||
|
||||
const skills = actor.items.filter(i => i.type === "skill").map(mapItem);
|
||||
const egos = actor.items.filter(i => i.type === "ego").map(mapItem);
|
||||
|
||||
let assignedSkill = null;
|
||||
if (assignedId) {
|
||||
const item = actor.items.get(assignedId);
|
||||
if (item) assignedSkill = { id: item.id, name: item.name, img: item.img };
|
||||
}
|
||||
|
||||
let selectedSkillPreview = null;
|
||||
if (this._previewSkillId) {
|
||||
const item = actor.items.get(this._previewSkillId);
|
||||
if (item) {
|
||||
const rawDice = item.system.dice?.die ?? [];
|
||||
const diceArr = Array.isArray(rawDice) ? rawDice : Object.values(rawDice);
|
||||
selectedSkillPreview = {
|
||||
name: item.name,
|
||||
dice: diceArr.map(d => ({
|
||||
type: d.type ?? "slash",
|
||||
formula: d.formula ?? "",
|
||||
mods: Array.isArray(d.mods) ? d.mods.filter(m => m) : Object.values(d.mods ?? {}).filter(m => m)
|
||||
})),
|
||||
modules: item.system.skill_modules?.mods
|
||||
? (Array.isArray(item.system.skill_modules.mods)
|
||||
? item.system.skill_modules.mods.filter(m => m).join(" | ")
|
||||
: String(item.system.skill_modules.mods))
|
||||
: ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const arrowStore = globalThis.sotcClashArrow?.ArrowStore ?? null;
|
||||
const sioIdx = this.sioMarkerIndex;
|
||||
const hasArrows = arrowStore && combatant?.tokenId
|
||||
? arrowStore.all().some(a => a.fromToken === combatant.tokenId && a.fromDie === sioIdx)
|
||||
: false;
|
||||
|
||||
return {
|
||||
tokenId: combatant.tokenId,
|
||||
combatantId: this.combatantId,
|
||||
actorId: actor.id,
|
||||
tokenName: combatant.name,
|
||||
tokenImg: actor.img,
|
||||
isClone: !!combatant.getFlag("sotc", "isSpeedDieClone"),
|
||||
dieNumber: this.dieNumber,
|
||||
hpValue, hpMax,
|
||||
hpPercent: Math.max(0, Math.min(100, Math.round((hpValue / hpMax) * 100))),
|
||||
staggerValue, staggerMax,
|
||||
staggerPercent: Math.max(0, Math.min(100, Math.round((staggerValue / staggerMax) * 100))),
|
||||
lightValue, lightMax,
|
||||
emotion,
|
||||
skills,
|
||||
egos,
|
||||
assignedSkill,
|
||||
selectedSkillPreview,
|
||||
dieState: this.dieState,
|
||||
activeTab: this._activeTab,
|
||||
hasArrows,
|
||||
};
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Listeners */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
||||
html.find(".open-sheet").click(ev => {
|
||||
const actorId = ev.currentTarget.dataset.actorId;
|
||||
const actor = game.actors.get(actorId);
|
||||
actor?.sheet?.render(true);
|
||||
});
|
||||
|
||||
html.find(".bsa-tab").click(ev => {
|
||||
const tab = ev.currentTarget.dataset.tab;
|
||||
this._activeTab = tab;
|
||||
html.find(".bsa-tab").removeClass("active");
|
||||
html.find(".bsa-tab-content").removeClass("active");
|
||||
ev.currentTarget.classList.add("active");
|
||||
html.find(`.bsa-tab-${tab}`).addClass("active");
|
||||
});
|
||||
|
||||
html.find(".bsa-skill-row").click(ev => {
|
||||
if (ev.target.closest(".bsa-btn-assign")) return;
|
||||
const skillId = ev.currentTarget.dataset.skillId;
|
||||
this._previewSkillId = (this._previewSkillId === skillId) ? null : skillId;
|
||||
this.render(false);
|
||||
});
|
||||
|
||||
html.find(".bsa-btn-assign").click(ev => {
|
||||
ev.stopPropagation();
|
||||
const skillId = ev.currentTarget.dataset.skillId;
|
||||
this._onAssignSkill(skillId);
|
||||
});
|
||||
|
||||
html.find(".bsa-btn-unassign").click(() => {
|
||||
this._onUnassignSkill();
|
||||
});
|
||||
|
||||
html.find(".bsa-btn-remove-arrows").click(() => {
|
||||
this._onRemoveArrows();
|
||||
});
|
||||
|
||||
html.find(".bsa-state-btn").click(ev => {
|
||||
const state = parseInt(ev.currentTarget.dataset.state);
|
||||
this._onSetDieState(state);
|
||||
});
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Actions */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
// Gérer l'assignation d'une compétence au dé de vitesse
|
||||
async _onAssignSkill(skillId) {
|
||||
const actor = this.actor;
|
||||
const combatant = this.combatant;
|
||||
if (!actor || !combatant) return;
|
||||
|
||||
const item = actor.items.get(skillId);
|
||||
if (!item) return;
|
||||
|
||||
if (this.assignedSkillId) {
|
||||
ui.notifications.warn(game.i18n.localize("SOTC_BSA.Panel.AlreadyAssigned"));
|
||||
return;
|
||||
}
|
||||
|
||||
const lightCost = item.system.light_cost ?? 0;
|
||||
const emotionCost = item.system.emotion_cost ?? 0;
|
||||
const currentLight = actor.system.light?.value ?? 0;
|
||||
const currentEmotion = actor.system.emotion ?? 0;
|
||||
|
||||
if (lightCost > 0 && currentLight < lightCost) {
|
||||
ui.notifications.error(
|
||||
game.i18n.format("SOTC_BSA.Panel.InsufficientLight", { cost: lightCost, current: currentLight })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (emotionCost > 0 && currentEmotion < emotionCost) {
|
||||
ui.notifications.error(
|
||||
game.i18n.format("SOTC_BSA.Panel.InsufficientEmotion", { cost: emotionCost, current: currentEmotion })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const updates = {};
|
||||
if (lightCost > 0) updates["system.light.value"] = Math.max(0, currentLight - lightCost);
|
||||
if (emotionCost > 0) updates["system.emotion"] = Math.max(0, currentEmotion - emotionCost);
|
||||
if (Object.keys(updates).length > 0) await actor.update(updates);
|
||||
|
||||
await combatant.setFlag("sotc-battle-show-ability", "assignedSkillId", skillId);
|
||||
// Toujours forcer l'état à 3 (Assigned) dès qu'une compétence est assignée
|
||||
await combatant.setFlag("sotc-battle-show-ability", "dieState", 3);
|
||||
await combatant.setFlag("sotc", "used", true);
|
||||
|
||||
// Mettre le marqueur SIO à l'état 3 (DiceSlotIcon4) pour indiquer l'assignation
|
||||
const tokenDocAssign = combatant.token;
|
||||
if (tokenDocAssign && game.modules.get("stars-initiative-overlay")?.active) {
|
||||
const dieIndexAssign = this.sioMarkerIndex;
|
||||
const numDiceAssign = combatant.actor?.system?.speed_dice?.num_dice ?? 1;
|
||||
const statesAssign = [...(tokenDocAssign.getFlag("stars-initiative-overlay", "diceStates") || [])];
|
||||
while (statesAssign.length < numDiceAssign) statesAssign.push(0);
|
||||
statesAssign[dieIndexAssign] = 3;
|
||||
await tokenDocAssign.setFlag("stars-initiative-overlay", "diceStates", statesAssign);
|
||||
}
|
||||
|
||||
ui.notifications.info(game.i18n.localize("SOTC_BSA.Panel.AssignSuccess"));
|
||||
this._previewSkillId = null;
|
||||
this.render(false);
|
||||
}
|
||||
|
||||
// Gérer la désassignation de la compétence du dé de vitesse
|
||||
async _onUnassignSkill() {
|
||||
const combatant = this.combatant;
|
||||
const actor = this.actor;
|
||||
if (!combatant || !actor) return;
|
||||
|
||||
const skillId = this.assignedSkillId;
|
||||
if (skillId) {
|
||||
const item = actor.items.get(skillId);
|
||||
if (item) {
|
||||
const lightCost = item.system.light_cost ?? 0;
|
||||
const emotionCost = item.system.emotion_cost ?? 0;
|
||||
const updates = {};
|
||||
|
||||
if (lightCost > 0) {
|
||||
const currentLight = actor.system.light?.value ?? 0;
|
||||
const maxLight = actor.system.light?.max ?? currentLight;
|
||||
updates["system.light.value"] = Math.min(currentLight + lightCost, maxLight);
|
||||
}
|
||||
if (emotionCost > 0) {
|
||||
const currentEmotion = actor.system.emotion ?? 0;
|
||||
updates["system.emotion"] = currentEmotion + emotionCost;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) await actor.update(updates);
|
||||
}
|
||||
}
|
||||
|
||||
// On force l'ordre : d'abord on retire l'assignation, puis on met l'état à 0
|
||||
await combatant.unsetFlag("sotc-battle-show-ability", "assignedSkillId");
|
||||
await combatant.setFlag("sotc-battle-show-ability", "dieState", 0);
|
||||
await combatant.setFlag("sotc", "used", false);
|
||||
|
||||
// Synchronisation SIO
|
||||
const tokenDoc = combatant.token;
|
||||
if (tokenDoc && game.modules.get("stars-initiative-overlay")?.active) {
|
||||
const dieIndex = this.sioMarkerIndex;
|
||||
const numDice = combatant.actor?.system?.speed_dice?.num_dice ?? 1;
|
||||
const states = [...(tokenDoc.getFlag("stars-initiative-overlay", "diceStates") || [])];
|
||||
while (states.length < numDice) states.push(0);
|
||||
states[dieIndex] = 0; // on force explicitement à Unused
|
||||
|
||||
await tokenDoc.setFlag("stars-initiative-overlay", "diceStates", states);
|
||||
}
|
||||
|
||||
this._previewSkillId = null;
|
||||
this.render(false);
|
||||
}
|
||||
|
||||
// Gérer le changement d'état du dé de vitesse (Unused, Used, Saved, Assigned)
|
||||
_onRemoveArrows() {
|
||||
const arrowStore = globalThis.sotcClashArrow?.ArrowStore ?? null;
|
||||
if (!arrowStore) { ui.notifications.warn("sotc-clash-arrow n'est pas actif."); return; }
|
||||
const combatant = this.combatant;
|
||||
if (!combatant) return;
|
||||
const tokenId = combatant.tokenId;
|
||||
const sioIdx = this.sioMarkerIndex;
|
||||
const toRemove = arrowStore.all()
|
||||
.filter(a => a.fromToken === tokenId && a.fromDie === sioIdx)
|
||||
.map(a => a.id);
|
||||
if (toRemove.length === 0) { ui.notifications.info("Aucune flèche à retirer pour ce dé."); return; }
|
||||
for (const id of toRemove) arrowStore.removeArrow(id);
|
||||
ui.notifications.info(`${toRemove.length} flèche(s) retirée(s).`);
|
||||
this.render(false);
|
||||
}
|
||||
|
||||
async _onSetDieState(state) {
|
||||
const combatant = this.combatant;
|
||||
if (!combatant) return;
|
||||
|
||||
// Si une compétence est assignée, l'état est verrouillé à 3
|
||||
const effectiveState = this.assignedSkillId ? 3 : state;
|
||||
|
||||
await combatant.setFlag("sotc-battle-show-ability", "dieState", effectiveState);
|
||||
|
||||
const isUsed = (effectiveState === 1 || effectiveState === 2 || effectiveState === 3);
|
||||
await combatant.setFlag("sotc", "used", isUsed);
|
||||
|
||||
// Synchroniser le flag SIO pour mettre à jour l'icône PIXI
|
||||
const tokenDoc = combatant.token;
|
||||
if (tokenDoc && game.modules.get("stars-initiative-overlay")?.active) {
|
||||
const dieIndex = this.sioMarkerIndex;
|
||||
const numDice = combatant.actor?.system?.speed_dice?.num_dice ?? 1;
|
||||
const currentStates = tokenDoc.getFlag("stars-initiative-overlay", "diceStates") || [];
|
||||
const states = [...currentStates];
|
||||
while (states.length < numDice) states.push(0);
|
||||
states[dieIndex] = effectiveState;
|
||||
await tokenDoc.setFlag("stars-initiative-overlay", "diceStates", states);
|
||||
}
|
||||
|
||||
ui.notifications.info(game.i18n.localize("SOTC_BSA.Panel.StateChanged"));
|
||||
this.render(false);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Validité */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
_checkStillValid() {
|
||||
if (!game.combat || !this.combatant) {
|
||||
this.close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue