559 lines
22 KiB
JavaScript
559 lines
22 KiB
JavaScript
/**
|
||
* 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 2025–2026 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) {
|
||
const content = await foundry.applications.handlebars.renderTemplate(template, context);
|
||
await ChatMessage.create({
|
||
speaker: ChatMessage.getSpeaker({ actor }),
|
||
user: game.user.id,
|
||
content
|
||
});
|
||
}
|
||
|
||
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 "Avantage";
|
||
if (mode === "desavantage") return "Desavantage";
|
||
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, 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(`Aucune faveur disponible pour ${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),
|
||
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(`Aucune faveur disponible pour ${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."
|
||
});
|
||
|
||
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,
|
||
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 ?? "Jet de caracteristique",
|
||
subtitle: result.characteristic.label,
|
||
formula: result.values.length > 1 ? "2d20" : "1d20",
|
||
mode: effectiveMode,
|
||
modeLabel: this.#getModeLabel(effectiveMode),
|
||
target: result.target,
|
||
targetPillLabel: "Cible",
|
||
targetPillValue: result.target,
|
||
values: result.values,
|
||
kept: result.kept,
|
||
keptPillLabel: "Garde",
|
||
keptPillValue: result.kept,
|
||
success: result.success,
|
||
favorLabel: favor?.label ?? null,
|
||
favorNote: favor?.note ?? null,
|
||
details: [
|
||
{ label: "Caracteristique", value: result.characteristic.label },
|
||
{ label: "Valeur cible", value: result.target },
|
||
...(favor ? [
|
||
{ label: "Faveur", value: favor.label },
|
||
{ label: "Dé de faveur", value: favor.result },
|
||
{ label: "Avant", value: DonjonEtCieUtility.formatUsageDie(favor.before) },
|
||
{ label: "Apres", value: DonjonEtCieUtility.formatUsageDie(favor.after) }
|
||
] : [])
|
||
]
|
||
});
|
||
|
||
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
|
||
});
|
||
|
||
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
|
||
});
|
||
|
||
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: "Jet",
|
||
keptPillValue: result.kept,
|
||
success: result.success,
|
||
favorLabel: favor?.label ?? null,
|
||
favorNote: favor?.note ?? null,
|
||
showDamageButton: result.success && Boolean(item.system.degats),
|
||
itemUuid: item.uuid,
|
||
details: [
|
||
{ label: "Arme", value: item.name },
|
||
{ label: "Caracteristique", value: characteristicLabel },
|
||
{ label: `Valeur de ${characteristicLabel}`, value: result.target },
|
||
{ label: "Degats", value: item.system.degats || "—" },
|
||
{ label: "Portee", value: item.system.portee || "—" },
|
||
...(favor ? [
|
||
{ label: "Faveur", value: favor.label },
|
||
{ label: "Dé de faveur", value: favor.result },
|
||
{ label: "Avant", value: DonjonEtCieUtility.formatUsageDie(favor.before) },
|
||
{ label: "Apres", value: DonjonEtCieUtility.formatUsageDie(favor.after) }
|
||
] : [])
|
||
]
|
||
});
|
||
|
||
return { ...result, favor, mode: effectiveMode };
|
||
}
|
||
|
||
static async rollDamage(actor, item, { mode = "normal" } = {}) {
|
||
if (!item.system.degats) return null;
|
||
const actorBonus = Number(actor?.system?.combat?.degatsBonus ?? 0);
|
||
const totalBonus = actorBonus;
|
||
const formula = totalBonus ? `${item.system.degats} + ${totalBonus}` : item.system.degats;
|
||
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);
|
||
|
||
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: item.system.degats,
|
||
sourceLabel: item.name,
|
||
targets,
|
||
hasTargets: targets.length > 0
|
||
});
|
||
|
||
return { total: result.kept, formula: result.formula, bonus: totalBonus, values: result.values, mode: result.mode };
|
||
}
|
||
|
||
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 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 currentPv = Number(actor.system.sante?.pv?.value ?? 0);
|
||
const availableMagicHp = currentPv + focus.activeValue;
|
||
|
||
if (cost > availableMagicHp) {
|
||
ui.notifications.warn("Le lanceur ne dispose pas d'assez de PV et de focus pour payer ce sort.");
|
||
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, focus.activeValue);
|
||
const focusRemaining = Math.max(focus.activeValue - 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: "Jet",
|
||
keptPillValue: result.kept,
|
||
success,
|
||
specialNote,
|
||
showDamageButton: success && Boolean(item.system.degats),
|
||
showChaosButton: canInvokeChaos,
|
||
itemUuid: item.uuid,
|
||
actorUuid: actor.uuid,
|
||
details: [
|
||
{ label: "Sortilege", value: item.name },
|
||
{ label: "Caracteristique", value: result.characteristic.label },
|
||
{ label: "Valeur de la caracteristique", value: result.target },
|
||
{ label: "Cout en PV", value: cost },
|
||
{ label: "Focus", value: focus.activeValue > 0 ? `${focus.activeValue} (${DonjonEtCieUtility.formatUsageDie(focus.before)})` : "—" },
|
||
{ label: "Focus depense", value: focusSpent },
|
||
{ label: "Focus restant", value: focusRemaining },
|
||
{ label: "PV depenses", value: spentPv },
|
||
{ label: "PV restants", value: remainingPv },
|
||
{ label: "Rang du lanceur", value: rank },
|
||
{ label: "Difficulte", value: item.system.difficulte ?? 0 },
|
||
{ label: "Effet", value: item.system.effet || "—" },
|
||
...(favor ? [
|
||
{ label: "Faveur", value: favor.label },
|
||
{ label: "Dé de faveur", value: favor.result },
|
||
{ label: "Avant", value: DonjonEtCieUtility.formatUsageDie(favor.before) },
|
||
{ label: "Apres", 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
|
||
});
|
||
|
||
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("Le Chaos n'est pas disponible pour ce sort.");
|
||
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
|
||
});
|
||
|
||
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
|
||
});
|
||
|
||
return { result, values: resolved.values, mode: resolved.mode, before, after, degraded };
|
||
}
|
||
}
|