/** * VermineTotemDice - Gestion des Dés de Totems (Humain / Adapté). * * Règles p. 121 : * - Influence : si un joueur possède strictement plus de Dés d'un Totem que * de l'autre, ce Totem prend l'ascendant (+1D/-1D sur certains Domaines). * - Gains et pertes via les Instincts et les Interdits (1D ou 2D selon la * gravité), avec des limites (3 Dés du même Totem, 5 Dés au maximum). */ export class VermineTotemDice { /** * Totem dominant d'un personnage. * @param {foundry.abstract.Document} actor * @returns {'human'|'adapted'|'neutral'} */ static getInfluence(actor) { const human = actor.system?.adaptation?.totems?.human?.value || 0; const adapted = actor.system?.adaptation?.totems?.adapted?.value || 0; if (human > adapted) return "human"; if (adapted > human) return "adapted"; return "neutral"; } /** * Modificateur (Bonus/Malus) d'influence pour une catégorie de compétence. * @param {foundry.abstract.Document} actor * @param {string} skillCategory catégorie (man, animal, tool, weapon, survival, world) * @returns {number} -1, 0 ou +1 */ static influenceModifier(actor, skillCategory) { const influence = this.getInfluence(actor); if (influence === "neutral" || !skillCategory) return 0; const cfg = CONFIG.VERMINE?.totemInfluence?.[influence]; if (!cfg) return 0; if ((cfg.bonus || []).includes(skillCategory)) return 1; if ((cfg.malus || []).includes(skillCategory)) return -1; return 0; } /** * Applique un gain d'un Dé de Totem selon les règles d'échange et de limites. * Retourne un objet décrivant ce qui s'est passé (pour notification). * @param {foundry.abstract.Document} actor * @param {'human'|'adapted'} totem * @param {number} [count=1] 1 ou 2 dés (acte grave) * @returns {Promise} */ static async gainDie(actor, totem, count = 1) { const human = actor.system.adaptation.totems.human; const adapted = actor.system.adaptation.totems.adapted; const actions = []; for (let i = 0; i < count; i++) { const step = this._gainOneDie(human.value, adapted.value, totem); if (step.unchanged) { actions.push({ ok: false, reason: "max" }); break; } human.value = step.human; adapted.value = step.adapted; actions.push({ ok: true, exchanged: step.exchanged, replaced: step.replaced }); } await actor.update({ "system.adaptation.totems.human.value": human.value, "system.adaptation.totems.adapted.value": adapted.value }); return this._summarize(actions, totem); } /** * Applique une perte d'un Dé de Totem. * Règle : seul l'Humain a des Interdits. Briser un Interdit Humain fait * perdre 1D Humain ; si le personnage n'en possède pas, il gagne 1D Adapté. * @param {foundry.abstract.Document} actor * @param {'human'} totem * @param {number} [count=1] 1 ou 2 dés (acte grave) * @returns {Promise} */ static async loseDie(actor, totem, count = 1) { const human = actor.system.adaptation.totems.human; const adapted = actor.system.adaptation.totems.adapted; const actions = []; for (let i = 0; i < count; i++) { if (human.value > 0) { human.value -= 1; actions.push({ ok: true, lost: true }); } else if (adapted.value < adapted.max && (human.value + adapted.value) < 5) { // Pas de Dé Humain : gagne un Dé Adapté à la place (limites appliquées). adapted.value += 1; actions.push({ ok: true, gainedAdapted: true }); } else { actions.push({ ok: false, reason: "max" }); break; } } await actor.update({ "system.adaptation.totems.human.value": human.value, "system.adaptation.totems.adapted.value": adapted.value }); return this._summarize(actions, totem); } /** * Ajoute un Dé du Totem donné en respectant les limites et les échanges. * Règles : * - 3 Dés max du même Totem, 5 Dés max au total. * - Si le joueur gagne un Dé qu'il ne peut conserver, il peut supprimer un * Dé du Totem opposé à la place ; sinon le gain est annulé. * - Gagner un 4e Dé Adapté est impossible, mais supprime un Dé Humain. * @param {number} human * @param {number} adapted * @param {'human'|'adapted'} totem * @returns {Object} */ static _gainOneDie(human, adapted, totem) { const limitTotem = 3; const opposite = totem === "human" ? "adapted" : "human"; const total = human + adapted; const current = totem === "human" ? human : adapted; const oppositeVal = totem === "human" ? adapted : human; // Le Totem cible est déjà au maximum (3) : impossible d'empiler. if (current >= limitTotem) { if (oppositeVal > 0) { // Supprime un Dé du Totem opposé à la place (le gain n'augmente pas). const result = { human, adapted, unchanged: false, replaced: true }; result[opposite] -= 1; return result; } return { human, adapted, unchanged: true }; } // Limite totale atteinte (5) : peut remplacer un Dé opposé. if (total >= 5) { if (oppositeVal > 0) { const result = { human, adapted, unchanged: false, exchanged: true }; result[totem] += 1; result[opposite] -= 1; return result; } return { human, adapted, unchanged: true }; } // Gain normal. const result = { human, adapted, unchanged: false }; result[totem] += 1; return result; } /** * Résume les actions appliquées en un libellé localisé. * @param {Array} actions * @param {'human'|'adapted'} totem * @returns {Object} */ static _summarize(actions, totem) { const gained = actions.filter(a => a.ok && !a.exchanged && !a.replaced && !a.lost && !a.gainedAdapted).length; const exchanged = actions.filter(a => a.exchanged).length; const replaced = actions.filter(a => a.replaced).length; const lost = actions.filter(a => a.lost).length; const gainedAdapted = actions.filter(a => a.gainedAdapted).length; const refused = actions.some(a => !a.ok); return { totem, gained, exchanged, replaced, lost, gainedAdapted, refused }; } }