Files
fvtt-donjon-et-cie/modules/donjon-et-cie-rolls.mjs
T
2026-05-22 09:50:48 +02:00

663 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Donjon & Cie - Systeme FoundryVTT
*
* Donjon & Cie est un jeu de role edite par John Doe.
* Ce systeme FoundryVTT est une implementation independante et n'est pas
* affilie a John Doe.
*
* @author LeRatierBretonnien
* @copyright 20252026 LeRatierBretonnien
* @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
*/
import { DonjonEtCieUtility } from "./donjon-et-cie-utility.mjs";
import { DONJON_ET_CIE } from "./donjon-et-cie-config.mjs";
export class DonjonEtCieRolls {
static async #createChatCard(actor, template, context, { rolls = [] } = {}) {
const content = await foundry.applications.handlebars.renderTemplate(template, context);
const validRolls = rolls.filter((roll) => roll instanceof Roll);
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor }),
user: game.user.id,
content,
rolls: validRolls
});
}
static #selectKeptValue(values, mode, favorable = "low") {
if (!values.length) return null;
if (mode === "normal") return values[0];
const selector = favorable === "low"
? (mode === "avantage" ? Math.min : Math.max)
: (mode === "avantage" ? Math.max : Math.min);
return selector(...values);
}
static #getModeLabel(mode) {
if (mode === "avantage") return game.i18n.localize("DNC.UI.ModeAdvantage");
if (mode === "desavantage") return game.i18n.localize("DNC.UI.ModeDisadvantage");
if (mode === "normal") return game.i18n.localize("DNC.UI.ModeNormal");
return null;
}
static #applyFavorMode(mode) {
if (mode === "desavantage") return "normal";
return "avantage";
}
static async #resolveFormulaRoll(formula, data = {}, { mode = "normal", favorable = "high" } = {}) {
const rollCount = mode === "normal" ? 1 : 2;
const rolls = await Promise.all(Array.from({ length: rollCount }, () => (new Roll(formula, data)).evaluate()));
const values = rolls.map((roll) => roll.total);
const kept = this.#selectKeptValue(values, mode, favorable);
const keptIndex = Math.max(0, values.findIndex((value) => value === kept));
const keptRoll = rolls[keptIndex] ?? rolls[0];
return { rolls, values, kept, keptIndex, keptRoll, mode, formula: keptRoll.formula };
}
static async #resolveCharacteristic(actor, characteristicKey, { mode = "normal" } = {}) {
const characteristic = actor.system.caracteristiques?.[characteristicKey];
if (!characteristic) return null;
const target = Number(characteristic.value ?? 0);
const rollCount = mode === "normal" ? 1 : 2;
const roll = await (new Roll(`${rollCount}d20`)).evaluate();
const values = roll.dice[0]?.results?.map((result) => result.result) ?? [];
const kept = this.#selectKeptValue(values, mode, "low");
const success = kept <= target;
return { characteristic, characteristicKey, target, values, kept, success, mode, roll, isNaturalOne: kept === 1, isNaturalTwenty: kept === 20 };
}
static async #resolveFavorBoost(actor, favorKey, mode = "normal") {
if (!favorKey) return null;
const label = DonjonEtCieUtility.getFavorLabel(favorKey);
const path = `system.faveurs.${favorKey}.delta`;
const before = Number(foundry.utils.getProperty(actor, path) ?? 0);
if (!before) {
ui.notifications.warn(game.i18n.format("DNC.Warn.NoFavorAvailable", { label }));
return null;
}
const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { favorable: "high" });
const result = resolved.kept;
const degraded = result <= 3;
const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
if (after !== before) {
await actor.update({ [path]: after });
}
return {
key: favorKey,
label,
before,
after,
result,
degraded,
stable: !degraded,
effectiveMode: this.#applyFavorMode(mode),
modeBefore: mode,
modeAfter: this.#applyFavorMode(mode),
rolls: resolved.rolls,
note: degraded
? "Le coup de pouce reste anonyme : un collegue du departement a donne l'info utile."
: "Le coup de pouce tient bon : nommez le collegue, ses trois traits et la relation pour le trombinoscope."
};
}
static async useFavorService(actor, favorKey) {
if (!favorKey) return null;
const label = DonjonEtCieUtility.getFavorLabel(favorKey);
const path = `system.faveurs.${favorKey}.delta`;
const before = Number(foundry.utils.getProperty(actor, path) ?? 0);
if (!before) {
ui.notifications.warn(game.i18n.format("DNC.Warn.NoFavorAvailable", { label }));
return null;
}
const after = DonjonEtCieUtility.degradeUsageDie(before);
await actor.update({ [path]: after });
await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/favor-card.hbs", {
title: game.i18n.localize("DNC.Roll.Favor"),
subtitle: label,
kindLabel: "Service",
before: DonjonEtCieUtility.formatUsageDie(before),
after: DonjonEtCieUtility.formatUsageDie(after),
autoSpent: true,
note: "La faveur est brulee pour obtenir directement l'aide souhaitee, a la discretion du MJ."
}, { rolls: [] });
return { key: favorKey, label, before, after };
}
static async #ensureFocus(actor) {
const focusDelta = Number(actor.system.magie?.focus?.delta ?? 0);
const focusResult = Number(actor.system.magie?.focus?.resultat ?? 0);
const focusSceneId = actor.system.magie?.focus?.sceneId ?? "";
const currentSceneId = DonjonEtCieUtility.getCurrentSceneId();
const sameScene = focusSceneId === currentSceneId;
const activeFocus = sameScene ? focusResult : 0;
if (!focusDelta) {
return { delta: 0, activeValue: 0, rolled: false, before: 0, after: 0, degraded: false };
}
if (sameScene) {
return { delta: focusDelta, activeValue: activeFocus, rolled: false, before: focusDelta, after: focusDelta, degraded: false };
}
const resolved = await this.#resolveFormulaRoll(`1d${focusDelta}`, {}, { favorable: "high" });
const result = resolved.kept;
const degraded = result <= 3;
const after = degraded ? DonjonEtCieUtility.degradeUsageDie(focusDelta) : focusDelta;
const updateData = {
"system.magie.focus.resultat": result,
"system.magie.focus.sceneId": currentSceneId
};
if (after !== focusDelta) {
updateData["system.magie.focus.delta"] = after;
}
await actor.update(updateData);
return {
delta: after,
activeValue: result,
rolled: true,
before: focusDelta,
after,
degraded,
rolls: resolved.rolls,
values: resolved.values
};
}
static async rollCharacteristic(actor, characteristicKey, { mode = "normal", label = null, favorKey = "" } = {}) {
const favor = await this.#resolveFavorBoost(actor, favorKey, mode);
const effectiveMode = favor?.effectiveMode ?? mode;
const result = await this.#resolveCharacteristic(actor, characteristicKey, { mode: effectiveMode });
if (!result) return null;
await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/roll-card.hbs", {
title: label ?? game.i18n.localize("DNC.Roll.Characteristic"),
subtitle: result.characteristic.label,
formula: result.values.length > 1 ? "2d20" : "1d20",
mode: effectiveMode,
modeLabel: this.#getModeLabel(effectiveMode),
target: result.target,
targetPillLabel: game.i18n.localize("DNC.Chat.Target"),
targetPillValue: result.target,
values: result.values,
kept: result.kept,
keptPillLabel: game.i18n.localize("DNC.Chat.Kept"),
keptPillValue: result.kept,
success: result.success,
favorLabel: favor?.label ?? null,
favorNote: favor?.note ?? null,
details: [
{ label: game.i18n.localize("DNC.UI.Characteristic"), value: result.characteristic.label },
{ label: game.i18n.localize("DNC.Chat.TargetValue"), value: result.target },
...(favor ? [
{ label: game.i18n.localize("DNC.Chat.Favor"), value: favor.label },
{ label: game.i18n.localize("DNC.Chat.FavorDie"), value: favor.result },
{ label: game.i18n.localize("DNC.Chat.Before"), value: DonjonEtCieUtility.formatUsageDie(favor.before) },
{ label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) }
] : [])
]
}, { rolls: [...(favor?.rolls ?? []), result.roll] });
return { ...result, favor, mode: effectiveMode };
}
static async rollInitiative(actor, { mode = "normal" } = {}) {
const dex = Number(actor.system.caracteristiques?.dexterite?.value ?? 0);
const sheetBonus = Number(actor.system.combat?.initiativeBonus ?? 0);
const result = await this.#resolveFormulaRoll("1d20 + @dex + @sheetBonus", { dex, sheetBonus }, { mode, favorable: "high" });
const dieValues = result.rolls.map((roll) => roll.dice[0]?.results?.[0]?.result ?? roll.total);
const die = dieValues[result.keptIndex] ?? dieValues[0] ?? result.kept;
let syncedCombat = null;
const activeCombat = game.combats?.contents?.find((combat) => combat.active);
const combatant = activeCombat?.combatants?.find((entry) => entry.actorId === actor.id);
if (combatant) {
await activeCombat.setInitiative(combatant.id, result.kept);
const ordered = [...activeCombat.combatants].sort((a, b) => (b.initiative ?? -Infinity) - (a.initiative ?? -Infinity));
syncedCombat = {
name: activeCombat.name,
initiative: result.kept,
rank: ordered.findIndex((entry) => entry.id === combatant.id) + 1,
total: ordered.length
};
}
await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/initiative-card.hbs", {
title: game.i18n.localize("DNC.Roll.Initiative"),
actorName: actor.name,
total: result.kept,
formula: result.rolls.length > 1 ? `2 × ${result.formula}` : result.formula,
die,
dieValues,
dex,
bonus: sheetBonus,
mode: result.mode,
modeLabel: this.#getModeLabel(result.mode),
syncedCombat
}, { rolls: result.rolls });
return { total: result.kept, die, dieValues, dex, bonus: sheetBonus, mode: result.mode, syncedCombat };
}
static async rollHitDice(actor) {
const formula = String(actor.system.sante?.dv ?? "").trim();
if (!formula) return null;
let roll;
try {
roll = await (new Roll(formula)).evaluate();
} catch (error) {
ui.notifications.error(`Formule de DV invalide : ${formula}`);
throw error;
}
const dieValues = roll.dice.flatMap((die) => die.results?.map((result) => result.result) ?? []);
await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/hit-dice-card.hbs", {
title: game.i18n.localize("DNC.Roll.HitDice"),
actorName: actor.name,
formula: roll.formula,
total: roll.total,
dieValues
}, { rolls: [roll] });
return { formula: roll.formula, total: roll.total, dieValues };
}
static async rollWeapon(actor, item, { mode = "normal", favorKey = "" } = {}) {
const characteristicKey = DonjonEtCieUtility.getWeaponCharacteristicKey(item.system.categorie);
const favor = await this.#resolveFavorBoost(actor, favorKey, mode);
const effectiveMode = favor?.effectiveMode ?? mode;
const result = await this.#resolveCharacteristic(actor, characteristicKey, { mode: effectiveMode });
if (!result) return null;
const characteristicLabel = DONJON_ET_CIE.characteristics[characteristicKey]?.label ?? characteristicKey;
const characteristicShort = DONJON_ET_CIE.characteristics[characteristicKey]?.short ?? characteristicKey;
await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/roll-card.hbs", {
title: `${game.i18n.localize("DNC.Roll.Attack")} : ${item.name}`,
subtitle: DONJON_ET_CIE.weaponCategoryOptions[item.system.categorie] ?? item.system.categorie,
formula: result.values.length > 1 ? "2d20" : "1d20",
mode: effectiveMode,
modeLabel: this.#getModeLabel(effectiveMode),
target: result.target,
targetPillLabel: characteristicShort,
targetPillValue: result.target,
values: result.values,
kept: result.kept,
keptPillLabel: game.i18n.localize("DNC.Chat.RollValue"),
keptPillValue: result.kept,
success: result.success,
favorLabel: favor?.label ?? null,
favorNote: favor?.note ?? null,
showDamageButton: result.success && Boolean(item.system.degats),
showAmmoButton: Number(item.system.munitionsDelta ?? 0) > 0,
itemUuid: item.uuid,
details: [
{ label: game.i18n.localize("DNC.UI.Weapon"), value: item.name },
{ label: game.i18n.localize("DNC.UI.Characteristic"), value: characteristicLabel },
{ label: `Valeur de ${characteristicLabel}`, value: result.target },
{ label: game.i18n.localize("DNC.UI.Damage"), value: item.system.degats || "—" },
{ label: game.i18n.localize("DNC.UI.Ammunition"), value: Number(item.system.munitionsDelta ?? 0) > 0 ? DonjonEtCieUtility.formatUsageDie(item.system.munitionsDelta) : "—" },
{ label: game.i18n.localize("DNC.UI.Range"), value: item.system.portee || "—" },
...(favor ? [
{ label: game.i18n.localize("DNC.Chat.Favor"), value: favor.label },
{ label: game.i18n.localize("DNC.Chat.FavorDie"), value: favor.result },
{ label: game.i18n.localize("DNC.Chat.Before"), value: DonjonEtCieUtility.formatUsageDie(favor.before) },
{ label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) }
] : [])
]
}, { rolls: [...(favor?.rolls ?? []), result.roll] });
return { ...result, favor, mode: effectiveMode };
}
static async rollWeaponAmmoUsage(item, { mode = "normal" } = {}) {
const before = Number(item?.system?.munitionsDelta ?? 0);
if (!before) {
ui.notifications.warn(game.i18n.localize("DNC.Warn.NoAmmunitionAvailable"));
return null;
}
const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { mode, favorable: "high" });
const result = resolved.kept;
const degraded = result <= 3;
const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
if (after !== before) {
await item.update({ "system.munitionsDelta": after });
}
await this.#createChatCard(item.actor, "systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs", {
title: `${game.i18n.localize("DNC.Roll.Ammunition")} : ${item.name}`,
value: result,
values: resolved.values,
mode: resolved.mode,
modeLabel: this.#getModeLabel(resolved.mode),
before: DonjonEtCieUtility.formatUsageDie(before),
after: DonjonEtCieUtility.formatUsageDie(after),
protectionStored: null,
degraded,
exhausted: after === 0,
isAmmunition: true
}, { rolls: resolved.rolls });
return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
}
static async rollDamage(actor, item, { mode = "normal" } = {}) {
const isUsageDie = Boolean(item.system.degatsEstUsageDe);
const degatsDelta = Number(item.system.degatsDelta ?? 0);
if (isUsageDie && !degatsDelta) {
ui.notifications.warn(game.i18n.localize("DNC.Warn.DamageExhausted"));
return null;
}
if (!isUsageDie && !item.system.degats) return null;
const damageContext = DonjonEtCieUtility.getMartialDamageContext(actor, item);
const actorBonus = Number(actor?.system?.combat?.degatsBonus ?? 0);
const totalBonus = actorBonus;
const effectiveDamage = damageContext.effectiveFormula || (isUsageDie ? `1d${degatsDelta}` : item.system.degats);
const formula = totalBonus ? `${effectiveDamage} + ${totalBonus}` : effectiveDamage;
const result = await this.#resolveFormulaRoll(formula, {}, { mode, favorable: "high" });
const targets = DonjonEtCieUtility.getSceneDamageTargets();
const rollDieLabels = result.rolls.map((roll) => {
const dieValues = roll.dice.flatMap((die) => die.results?.map((dieResult) => dieResult.result) ?? []);
return dieValues.length ? dieValues.join(" + ") : String(roll.total ?? "—");
});
const keptDieLabel = rollDieLabels[result.keptIndex] ?? rollDieLabels[0] ?? String(result.kept);
const baseDamageDisplay = isUsageDie ? DonjonEtCieUtility.formatUsageDie(degatsDelta) : item.system.degats;
await this.#createChatCard(actor ?? item.actor, "systems/fvtt-donjon-et-cie/templates/chat/damage-card.hbs", {
title: `${game.i18n.localize("DNC.Roll.Damage")} : ${item.name}`,
subtitle: item.system.portee || item.type,
formula: result.rolls.length > 1 ? `2 × ${result.formula}` : result.formula,
mode: result.mode,
modeLabel: this.#getModeLabel(result.mode),
rollDieLabels,
keptDieLabel,
values: result.values,
total: result.kept,
bonus: totalBonus,
baseDamage: baseDamageDisplay,
effectiveDamage,
damageCapped: damageContext.capped,
martialDvLabel: damageContext.martialDvSides ? `d${damageContext.martialDvSides}` : damageContext.martialDvFormula,
sourceLabel: item.name,
targets,
hasTargets: targets.length > 0,
showDamageUsageButton: isUsageDie && degatsDelta > 0,
itemUuid: item.uuid
}, { rolls: result.rolls });
return {
total: result.kept,
formula: result.formula,
baseDamage: baseDamageDisplay,
effectiveDamage,
damageCapped: damageContext.capped,
bonus: totalBonus,
values: result.values,
mode: result.mode
};
}
static async rollWeaponDamageUsage(item, { mode = "normal" } = {}) {
const before = Number(item?.system?.degatsDelta ?? 0);
if (!before) {
ui.notifications.warn(game.i18n.localize("DNC.Warn.DamageExhausted"));
return null;
}
const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { mode, favorable: "high" });
const result = resolved.kept;
const degraded = result <= 3;
const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
if (after !== before) {
await item.update({ "system.degatsDelta": after });
}
await this.#createChatCard(item.actor, "systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs", {
title: `${game.i18n.localize("DNC.Roll.DamageUsage")} : ${item.name}`,
value: result,
values: resolved.values,
mode: resolved.mode,
modeLabel: this.#getModeLabel(resolved.mode),
before: DonjonEtCieUtility.formatUsageDie(before),
after: DonjonEtCieUtility.formatUsageDie(after),
protectionStored: null,
degraded,
exhausted: after === 0,
isDamageUsage: true
}, { rolls: resolved.rolls });
return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
}
static async applyDamage(target, { damage = 0, useArmor = false, sourceLabel = "" } = {}) {
const actor = target?.actor ?? target;
if (!actor || actor.documentName !== "Actor") {
ui.notifications.warn(game.i18n.localize("DNC.Chat.InvalidDamageTarget"));
return null;
}
const targetName = target?.name ?? actor.name;
const applied = await actor.applyIncomingDamage(damage, { useArmor });
await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/damage-application-card.hbs", {
title: game.i18n.localize("DNC.Chat.DamageApplied"),
subtitle: targetName,
sourceLabel,
total: applied.hpDamage,
incoming: applied.incoming,
useArmor: applied.useArmor,
armorLabel: applied.armorLabel,
armorAvailable: applied.armorAvailable,
armorBefore: applied.armorBefore,
armorAbsorbed: applied.armorAbsorbed,
armorAfter: applied.armorAfter,
pvBefore: applied.pvBefore,
pvAfter: applied.pvAfter,
pvMax: applied.pvMax
});
return { actor, targetName, ...applied };
}
static async rollSpell(actor, item, { mode = "normal", favorKey = "" } = {}) {
const characteristicKey = item.system.caracteristique || "intelligence";
const focus = await this.#ensureFocus(actor);
const currentSceneId = DonjonEtCieUtility.getCurrentSceneId();
const rank = Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0);
const cost = Number(item.system.coutPv ?? 0);
const autoDisadvantage = cost > rank;
const baseMode = autoDisadvantage ? "desavantage" : mode;
const favor = await this.#resolveFavorBoost(actor, favorKey, baseMode);
const effectiveMode = favor?.effectiveMode ?? baseMode;
const result = await this.#resolveCharacteristic(actor, characteristicKey, { mode: effectiveMode });
if (!result) return null;
const liveActor = game.actors.get(actor.id) ?? actor;
const currentPv = Number(liveActor.system.sante?.pv?.value ?? 0);
const currentFocusSceneId = liveActor.system.magie?.focus?.sceneId ?? "";
const currentFocusValue = currentFocusSceneId === currentSceneId
? Number(liveActor.system.magie?.focus?.resultat ?? 0)
: 0;
const availableMagicHp = currentPv + currentFocusValue;
if (cost > availableMagicHp) {
ui.notifications.warn(game.i18n.localize("DNC.Warn.SpellInsufficientResources"));
return null;
}
const characteristicShort = DONJON_ET_CIE.characteristics[characteristicKey]?.short ?? characteristicKey;
const success = result.isNaturalTwenty ? false : result.success;
const focusSpent = result.isNaturalOne ? 0 : Math.min(cost, currentFocusValue);
const focusRemaining = Math.max(currentFocusValue - focusSpent, 0);
const spentPv = result.isNaturalOne ? 0 : Math.max(cost - focusSpent, 0);
const remainingPv = Math.max(currentPv - spentPv, 0);
const updateData = {};
if (spentPv !== 0) {
updateData["system.sante.pv.value"] = remainingPv;
}
if (focusSpent !== 0) {
updateData["system.magie.focus.resultat"] = focusRemaining;
}
if (Object.keys(updateData).length) {
await actor.update(updateData);
}
const canInvokeChaos = !success && !result.isNaturalTwenty && Number(actor.system.magie?.chaos?.delta ?? 12) >= 4;
const specialNote = result.isNaturalTwenty
? "20 naturel : la magie tourne a la catastrophe, au choix du MJ."
: (result.isNaturalOne ? "1 naturel : effet benefique possible ; par defaut, aucun PV n'est depense." : null);
await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/spell-card.hbs", {
title: `${game.i18n.localize("DNC.Roll.Spell")} : ${item.name}`,
subtitle: item.system.portee || "Sortilege",
formula: result.values.length > 1 ? "2d20" : "1d20",
mode: effectiveMode,
modeLabel: this.#getModeLabel(effectiveMode),
autoDisadvantage,
autoDisadvantageCanceled: autoDisadvantage && Boolean(favor),
favorLabel: favor?.label ?? null,
favorNote: favor?.note ?? null,
targetPillLabel: characteristicShort,
targetPillValue: result.target,
values: result.values,
kept: result.kept,
keptPillLabel: game.i18n.localize("DNC.Chat.RollValue"),
keptPillValue: result.kept,
success,
specialNote,
showDamageButton: success && Boolean(item.system.degats),
showChaosButton: canInvokeChaos,
itemUuid: item.uuid,
actorUuid: actor.uuid,
details: [
{ label: game.i18n.localize("DNC.UI.Spell"), value: item.name },
{ label: game.i18n.localize("DNC.UI.Characteristic"), value: result.characteristic.label },
{ label: game.i18n.localize("DNC.Chat.CharacteristicValue"), value: result.target },
{ label: game.i18n.localize("DNC.Chat.HpCost"), value: cost },
{ label: game.i18n.localize("DNC.UI.Focus"), value: focus.activeValue > 0 ? `${focus.activeValue} (${DonjonEtCieUtility.formatUsageDie(focus.before)})` : "—" },
{ label: game.i18n.localize("DNC.Chat.FocusSpent"), value: focusSpent },
{ label: game.i18n.localize("DNC.Chat.FocusRemaining"), value: focusRemaining },
{ label: game.i18n.localize("DNC.Chat.HpSpent"), value: spentPv },
{ label: game.i18n.localize("DNC.Chat.HpRemaining"), value: remainingPv },
{ label: game.i18n.localize("DNC.Chat.CasterRank"), value: rank },
{ label: game.i18n.localize("DNC.Chat.Difficulty"), value: item.system.difficulte ?? 0 },
{ label: game.i18n.localize("DNC.Chat.Effect"), value: item.system.effet || "—" },
...(favor ? [
{ label: game.i18n.localize("DNC.Chat.Favor"), value: favor.label },
{ label: game.i18n.localize("DNC.Chat.FavorDie"), value: favor.result },
{ label: game.i18n.localize("DNC.Chat.Before"), value: DonjonEtCieUtility.formatUsageDie(favor.before) },
{ label: game.i18n.localize("DNC.Chat.After"), value: DonjonEtCieUtility.formatUsageDie(favor.after) }
] : [])
],
focusRolled: focus.rolled,
focusValue: focus.activeValue,
focusSpent,
focusRemaining,
focusBeforeLabel: DonjonEtCieUtility.formatUsageDie(focus.before),
focusAfterLabel: DonjonEtCieUtility.formatUsageDie(focus.after),
focusDegraded: focus.degraded,
spentPv,
remainingPv
}, { rolls: [...(favor?.rolls ?? []), ...(focus.rolls ?? []), result.roll] });
return { ...result, success, spentPv, remainingPv, cost, focus, focusSpent, focusRemaining, favor, mode: effectiveMode };
}
static async rollSpellChaos(actor, item) {
const before = Number(actor?.system?.magie?.chaos?.delta ?? 12);
if (!before || before < 4) {
ui.notifications.warn(game.i18n.localize("DNC.Warn.ChaosUnavailable"));
return null;
}
const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { favorable: "high" });
const result = resolved.kept;
const degraded = result <= 3;
const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
const chaosEntry = DONJON_ET_CIE.chaosTable[result] ?? null;
if (after !== before) {
await actor.update({ "system.magie.chaos.delta": after });
}
await this.#createChatCard(actor, "systems/fvtt-donjon-et-cie/templates/chat/chaos-card.hbs", {
title: `Chaos : ${item.name}`,
value: result,
before: DonjonEtCieUtility.formatUsageDie(before),
after: DonjonEtCieUtility.formatUsageDie(after),
chaosEntry,
degraded,
exhausted: after < 4,
itemName: item.name
}, { rolls: resolved.rolls });
return { result, before, after, degraded, chaosEntry };
}
static async rollUsage(item, { mode = "normal" } = {}) {
const before = Number(item.system.delta ?? 0);
if (!before) return null;
const resolved = await this.#resolveFormulaRoll(`1d${before}`, {}, { mode, favorable: "high" });
const result = resolved.kept;
const degraded = result <= 3;
const after = degraded ? DonjonEtCieUtility.degradeUsageDie(before) : before;
const updateData = {};
if (item.type === "armure") {
updateData["system.resultatProtection"] = result;
}
if (after !== before) {
updateData["system.delta"] = after;
}
if (Object.keys(updateData).length) {
await item.update(updateData);
}
await this.#createChatCard(item.actor, "systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs", {
title: `${game.i18n.localize("DNC.Roll.Usage")} : ${item.name}`,
value: result,
values: resolved.values,
mode: resolved.mode,
modeLabel: this.#getModeLabel(resolved.mode),
before: DonjonEtCieUtility.formatUsageDie(before),
after: DonjonEtCieUtility.formatUsageDie(after),
protectionStored: item.type === "armure" ? result : null,
degraded,
exhausted: after === 0
}, { rolls: resolved.rolls });
return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
}
}