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

308 lines
11 KiB
JavaScript
Raw 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 { DONJON_ET_CIE } from "./donjon-et-cie-config.mjs";
export class DonjonEtCieUtility {
static defaultItemIcons = {
arme: "systems/fvtt-donjon-et-cie/assets/icons/system/items/arme.svg",
armure: "systems/fvtt-donjon-et-cie/assets/icons/system/items/armure.svg",
trait: "systems/fvtt-donjon-et-cie/assets/icons/system/items/trait.svg",
sortilege: "systems/fvtt-donjon-et-cie/assets/icons/system/items/sortilege.svg",
equipement: "systems/fvtt-donjon-et-cie/assets/icons/system/items/equipement.svg",
entrainement: "systems/fvtt-donjon-et-cie/assets/icons/system/items/capacite.svg",
other: "systems/fvtt-donjon-et-cie/assets/icons/system/items/autre.svg"
};
static async preloadHandlebarsTemplates() {
return foundry.applications.handlebars.loadTemplates([
"systems/fvtt-donjon-et-cie/templates/actors/employe-sheet.hbs",
"systems/fvtt-donjon-et-cie/templates/actors/pnj-sheet.hbs",
"systems/fvtt-donjon-et-cie/templates/items/item-sheet.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/characteristic-roll.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/initiative-roll.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/weapon-roll.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/damage-roll.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/spell-roll.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/usage-roll.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-dialog.hbs",
"systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-campaign-dialog.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/roll-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/spell-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/chaos-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/hit-dice-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/damage-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/damage-application-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/favor-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/initiative-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/item-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/mission-pack-card.hbs",
"systems/fvtt-donjon-et-cie/templates/chat/welcome-card.hbs"
]);
}
static getCharacteristicEntries(system) {
return Object.entries(DONJON_ET_CIE.characteristics).map(([key, metadata]) => ({
key,
label: metadata.label,
short: metadata.short,
value: system.caracteristiques?.[key]?.value ?? 0
}));
}
static formatUsageDie(value) {
return value ? `Δ${value}` : "—";
}
static getDefaultItemIcon(type) {
return this.defaultItemIcons[type] ?? this.defaultItemIcons.other;
}
static getCurrentSceneId() {
return canvas?.scene?.id ?? game.scenes?.current?.id ?? "global";
}
static getSceneDamageTargets() {
const scene = canvas?.scene ?? game.scenes?.current;
const tokens = scene?.tokens?.contents ?? [];
return tokens
.map((token) => {
const actor = token.actor;
if (!actor || !["employe", "pnj"].includes(actor.type)) return null;
const tokenName = token.name || actor.name;
const actorName = actor.name || tokenName;
const label = tokenName === actorName ? tokenName : `${tokenName} (${actorName})`;
return {
tokenId: token.id,
tokenUuid: token.uuid,
actorUuid: actor.uuid,
label
};
})
.filter(Boolean)
.sort((a, b) => a.label.localeCompare(b.label, "fr", { sensitivity: "base" }));
}
static getMagicResourceContext(actor) {
const rank = Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0);
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 activeFocus = focusSceneId === this.getCurrentSceneId() ? focusResult : 0;
const chaosDelta = Number(actor.system.magie?.chaos?.delta ?? 12);
return {
rank,
focusDelta,
focusLabel: this.formatUsageDie(focusDelta),
focusSceneId,
focusStoredResult: focusResult,
focusActiveValue: activeFocus,
focusIsActive: activeFocus > 0,
focusDisplay: activeFocus > 0 ? `${activeFocus} (${this.formatUsageDie(focusDelta)})` : "—",
chaosDelta,
chaosLabel: this.formatUsageDie(chaosDelta),
chaosAvailable: chaosDelta >= 4
};
}
static parseDieFormula(formula) {
const normalized = String(formula ?? "").trim();
if (!normalized) return null;
const dieMatch = normalized.match(/(?<count>\d*)d(?<sides>\d+)/i);
if (dieMatch?.groups?.sides) {
return {
formula: normalized,
count: Number(dieMatch.groups.count || 1),
countRaw: dieMatch.groups.count ?? "",
sides: Number(dieMatch.groups.sides),
match: dieMatch[0],
index: dieMatch.index ?? 0
};
}
if (/^\d+$/.test(normalized)) {
const sides = Number(normalized);
if ([4, 6, 8, 10, 12, 20].includes(sides)) {
return {
formula: normalized,
count: 1,
countRaw: "",
sides,
match: normalized,
index: 0
};
}
}
return null;
}
static getMartialDamageContext(actor, item) {
const isUsageDie = Boolean(item?.system?.degatsEstUsageDe);
const degatsDelta = Number(item?.system?.degatsDelta ?? 0);
const baseFormula = isUsageDie
? (degatsDelta > 0 ? `1d${degatsDelta}` : "")
: String(item?.system?.degats ?? "").trim();
const baseContext = {
baseFormula,
effectiveFormula: baseFormula,
capped: false,
martialDvFormula: String(actor?.system?.sante?.dv ?? "").trim(),
martialDvSides: 0,
weaponSides: 0,
isUsageDie
};
if (actor?.type !== "employe" || item?.type !== "arme" || !baseFormula) {
return baseContext;
}
const damageDie = this.parseDieFormula(baseFormula);
const martialDie = this.parseDieFormula(actor.system.sante?.dv);
if (!damageDie || !martialDie?.sides) return baseContext;
const cappedSides = Math.min(damageDie.sides, martialDie.sides);
if (cappedSides >= damageDie.sides) {
return {
...baseContext,
martialDvSides: martialDie.sides,
weaponSides: damageDie.sides
};
}
const replacement = `${damageDie.countRaw || ""}d${cappedSides}`;
const effectiveFormula = damageDie.match === damageDie.formula
? replacement
: `${damageDie.formula.slice(0, damageDie.index)}${replacement}${damageDie.formula.slice(damageDie.index + damageDie.match.length)}`;
return {
...baseContext,
effectiveFormula,
capped: true,
martialDvSides: martialDie.sides,
weaponSides: damageDie.sides
};
}
static getFavorLabel(key) {
return DONJON_ET_CIE.favorDepartments[key] ?? key;
}
static getFavorEntries(system) {
const favors = system.faveurs ?? {};
return Object.entries(DONJON_ET_CIE.favorDepartments).map(([key, label]) => {
const delta = Number(favors[key]?.delta ?? 0);
return {
key,
label,
delta,
deltaLabel: this.formatUsageDie(delta),
hasFavor: delta > 0
};
});
}
static getAvailableFavorOptions(actor) {
return this.getFavorEntries(actor.system)
.filter((entry) => entry.hasFavor)
.map((entry) => ({ value: entry.key, label: `${entry.label} (${entry.deltaLabel})` }));
}
static getChaosTableEntries() {
return Object.entries(DONJON_ET_CIE.chaosTable)
.map(([value, entry]) => ({ value: Number(value), ...entry }))
.sort((a, b) => a.value - b.value);
}
static degradeUsageDie(value) {
const sequence = [12, 10, 8, 6, 4];
const index = sequence.indexOf(Number(value));
if (index === -1) return 0;
return sequence[index + 1] ?? 0;
}
static sortByName(documents) {
return [...documents].sort((a, b) => a.name.localeCompare(b.name, "fr", { sensitivity: "base" }));
}
static getWeaponCharacteristicKey(category) {
return category === "distance" ? "dexterite" : "force";
}
static getWeaponCharacteristicLabel(category) {
const key = this.getWeaponCharacteristicKey(category);
return DONJON_ET_CIE.characteristics[key]?.label ?? key;
}
static enrichItemForSheet(item) {
const system = item.system;
const delta = Number(system.delta ?? 0);
const deltaMax = Number(system.deltaMax ?? delta ?? 0);
const ammunitionDelta = Number(system.munitionsDelta ?? 0);
const isUsageDie = Boolean(system.degatsEstUsageDe);
const degatsDelta = Number(system.degatsDelta ?? 0);
const usageLabel = item.type === "entrainement" && deltaMax > 0
? `${this.formatUsageDie(delta)} / ${this.formatUsageDie(deltaMax)}`
: delta > 0
? this.formatUsageDie(delta)
: null;
const damageUsageLabel = isUsageDie
? (degatsDelta > 0 ? this.formatUsageDie(degatsDelta) : game.i18n.localize("DNC.UI.DamageExhausted"))
: null;
const damageLabel = isUsageDie ? damageUsageLabel : (system.degats || null);
return {
id: item.id,
name: item.name,
type: item.type,
img: item.img,
system,
uuid: item.uuid,
usageLabel,
ammunitionUsageLabel: item.type === "arme" && ammunitionDelta > 0 ? this.formatUsageDie(ammunitionDelta) : null,
protectionLabel: item.type === "armure" && Number(system.resultatProtection ?? 0) > 0 ? `Protection ${system.resultatProtection}` : null,
weaponCharacteristicLabel: item.type === "arme" ? this.getWeaponCharacteristicLabel(system.categorie) : null,
canRoll: ["arme", "sortilege"].includes(item.type),
canUse: delta > 0,
hasTrackedAmmunition: item.type === "arme" && ammunitionDelta > 0,
damageUsageLabel,
damageLabel,
canRollDamage: item.type === "arme" && (isUsageDie ? degatsDelta > 0 : Boolean(system.degats)),
rollAction: item.type === "sortilege" ? "rollSpell" : "rollWeapon",
damageAction: "rollDamage",
isEquipped: Boolean(system.equipee),
canReset: item.type === "entrainement" && deltaMax > 0 && delta !== deltaMax
};
}
static buildActorSections(actor) {
return Object.entries(DONJON_ET_CIE.actorSections).map(([key, metadata]) => {
const types = DONJON_ET_CIE.sectionTypes[key];
const items = this.sortByName(actor.items.filter((item) => types.includes(item.type))).map((item) => this.enrichItemForSheet(item));
return {
key,
label: metadata.label,
createType: metadata.createType,
items
};
});
}
}