Files
vermine2047/module/system/experience.mjs
T
uberwald b0a3aff648
Release Creation / build (release) Failing after 1m32s
feat: gestion des Dés de Totems (influence, instincts, interdits)
- VermineTotemDice : influence Humain/Adapté (+1D/-1D par domaine, règles
  p. 121), gains/pertes via Instincts/Interdits avec limites (3D/totem,
  5D total) et règles d'échange.
- roll.mjs applique l'influence selon le Totem dominant (au lieu de
  l'ancienne logique basée sur identity.totem).
- Dialogue TotemDiceDialog (instinct Humain/autre totem, interdit Humain,
  acte grave ±2D) + bouton et ligne d'influence sur la fiche personnage.
- Dés de Totem gagnés en cas d'échec à l'apprentissage Dé d'Évolution
  (dés dépensés, garde capacité vide).
- Couleur de texte @color-text-light-2 assombrie (#c9e0c0 → #3f9b55)
  pour la lisibilité dans tous les dialogues.

fix: robustesse et visibilité des évolutions

- buyEvolutionDialog : n'efface les Dés d'Évolution que si l'item est créé.
- Flag mutation éditable sur la fiche item evolution + badge
  Adaptation/Mutation dans la liste du personnage.
- loseDie : applique aussi la limite totale de 5 dés au gain Adapté.
- Case "acte grave" conservée entre les rendus du dialogue.
2026-08-06 15:32:55 +02:00

295 lines
9.3 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.
/**
* Règles d'apprentissage (phase d'Expérience).
* Helpers purs de difficultés, handicaps et calculs de jets d'Expérience.
*/
export class VermineExperience {
/**
* Difficulté d'un jet d'apprentissage de Compétence selon le niveau visé.
* @param {number} targetLevel niveau visé (1-5)
* @returns {number}
*/
static skillLearningDifficulty(targetLevel) {
switch (parseInt(targetLevel, 10)) {
case 1: return 5 // Débutant
case 2: return 7 // Confirmé
case 3: return 9 // Expert
case 4: return 10 // Maître
case 5: return 10 // Légende
default: return 7
}
}
/**
* Handicap d'un jet d'apprentissage de Compétence.
* Rareté (1-3) + handicap (I) supplémentaire pour le niveau Légende.
* @param {number} targetLevel niveau visé (1-5)
* @param {number} rarity rareté de la compétence (0-3)
* @returns {number}
*/
static skillLearningHandicap(targetLevel, rarity) {
let handicap = parseInt(rarity, 10) || 0
if (parseInt(targetLevel, 10) === 5) handicap += 1 // Légende : (I)
return handicap
}
/**
* Difficulté d'un jet d'apprentissage de Spécialité selon le niveau actuel
* de la compétence associée. Domaine de prédilection : -2.
* @param {number} skillLevel niveau actuel de la compétence (2-5)
* @param {boolean} isDomain la compétence est dans le domaine de prédilection
* @returns {number}
*/
static specialtyLearningDifficulty(skillLevel, isDomain) {
let difficulty
switch (parseInt(skillLevel, 10)) {
case 2: difficulty = 9 // Confirmé
break
case 3: difficulty = 7 // Expert
break
case 4: difficulty = 5 // Maître
break
case 5: difficulty = 5 // Légende (relance automatique)
break
default: difficulty = 9
}
return isDomain ? difficulty - 2 : difficulty
}
/**
* Handicap d'un jet d'apprentissage de Spécialité : nombre de spécialités
* déjà possédées (requiert au moins Confirmé dans la compétence associée).
* @param {number} specialtyCount
* @returns {number}
*/
static specialtyLearningHandicap(specialtyCount) {
return Math.max(0, parseInt(specialtyCount, 10) || 0)
}
/**
* Difficulté d'un jet d'apprentissage de Caractéristique.
* @returns {number}
*/
static characteristicLearningDifficulty() {
return 9
}
/**
* Difficulté d'un jet d'apprentissage de Réserve : valeur actuelle.
* @param {number} currentValue
* @returns {number}
*/
static reserveLearningDifficulty(currentValue) {
return Math.max(0, parseInt(currentValue, 10) || 0)
}
/**
* Handicap d'âge pour l'apprentissage des Réserves et autres jets liés à l'âge.
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
* @returns {number}
*/
static ageHandicap(ageType) {
switch (parseInt(ageType, 10)) {
case 1: return 1 // Jeune (I)
case 3: return 3 // Ancien (III)
default: return 2 // Adulte (II)
}
}
/**
* Difficulté d'un jet d'acquisition d'un Dé d'Évolution selon l'âge.
* @param {number} ageType 1=Jeune, 2=Adulte, 3=Ancien
* @returns {number}
*/
static evolutionDifficulty(ageType) {
switch (parseInt(ageType, 10)) {
case 1: return 5
case 3: return 9
default: return 7
}
}
/**
* Bonus/Malus de Totem sur le jet de Dé d'Évolution.
* Plus de Dés Adaptés que Humains : +1D. Inversement : -1D.
* @param {number} human human totem dice
* @param {number} adapted adapted totem dice
* @returns {number}
*/
static evolutionTotemModifier(human, adapted) {
if (adapted > human) return 1
if (human > adapted) return -1
return 0
}
/**
* Un Dé de Totem Adapté peut remplacer un Dé d'Expérience (compte ×2)
* uniquement si le personnage possède strictement plus de Dés Adaptés
* que Humains.
* @param {number} human human totem dice
* @param {number} adapted adapted totem dice
* @returns {boolean}
*/
static canUseAdaptedDie(human, adapted) {
return adapted > human
}
/**
* Difficulté d'un jet d'apprentissage de Capacité de Totem : valeur
* d'Apprentissage de la capacité.
* @param {object} abilityData system data de l'item ability
* @returns {number}
*/
static totemAbilityLearningDifficulty(abilityData) {
return parseInt(abilityData?.learn?.threshold, 10) || 5
}
/**
* Handicap d'un jet d'apprentissage de Capacité de Totem : valeur
* d'Apprentissage de la capacité.
* @param {object} abilityData system data de l'item ability
* @returns {number}
*/
static totemAbilityLearningHandicap(abilityData) {
return parseInt(abilityData?.learn?.hindrance, 10) || 0
}
/**
* Difficulté d'un jet d'obtention d'un Élément d'Historique.
* @returns {number}
*/
static backgroundLearningDifficulty() {
return 9
}
/**
* Handicap d'un jet d'obtention d'un Élément d'Historique : (I) si
* l'Historique est listé avec un astérisque.
* @param {boolean} asterisk
* @returns {number}
*/
static backgroundLearningHandicap(asterisk) {
return asterisk ? 1 : 0
}
/**
* Difficulté d'un jet visant à réduire le score de Réputation (devenir
* plus célèbre) : 10 moins le score que le personnage souhaite atteindre.
* @param {number} current réputation actuelle
* @returns {number}
*/
static reputationReduceDifficulty(current) {
return 10 - Math.max(0, (parseInt(current, 10) || 0) - 1)
}
/**
* Difficulté d'un jet visant à augmenter le score de Réputation (se faire
* oublier) : égale à la Réputation actuelle.
* @param {number} current réputation actuelle
* @returns {number}
*/
static reputationIncreaseDifficulty(current) {
return parseInt(current, 10) || 0
}
/**
* Clé i18n du libellé du niveau de compétence visé.
* @param {number} level
* @returns {string}
*/
static skillLevelLabelKey(level) {
return CONFIG.VERMINE.SkillLevels?.[level]?.label ?? "SKILL_LEVELS.incompetent"
}
/**
* Détermine si une compétence appartient au domaine de prédilection.
* @param {Actor} actor
* @param {string} skillKey
* @returns {boolean}
*/
static isPreferredDomain(actor, skillKey) {
const category = actor.system?.skills?.[skillKey]?.category
const preferred = actor.system?.skill_categories?.preferred
return Boolean(category && preferred && category === preferred)
}
/**
* Réinitialise les limites d'apprentissage (learned.*) de la phase
* courante, pour un personnage ou un Groupe, sans toucher au compteur
* de phase (avancé uniquement par le dialogue d'attribution du MJ).
* @param {foundry.abstract.Document} actor personnage ou Groupe
* @returns {Promise<void>}
*/
static async resetPhaseLimits(actor) {
const ex = actor.system?.experience
if (!ex) return
const update = {}
if (ex.learned && typeof ex.learned === "object") {
for (const key of Object.keys(ex.learned)) {
if (key === "phase") continue
update[`system.experience.learned.${key}`] = false
}
}
if (Object.keys(update).length) await actor.update(update)
}
/**
* Mode de jeu actif (1=Survie, 2=Cauchemar, 3=Apocalypse).
* @returns {number}
*/
static gameMode() {
return parseInt(game.settings.get("vermine2047", "game-mode"), 10) || 1
}
/**
* Total de Dés d'Évolution maximum selon le Mode de jeu.
* @param {number} mode 1=Survie, 2=Cauchemar, 3=Apocalypse
* @returns {number}
*/
static evolutionMaxForMode(mode) {
switch (parseInt(mode, 10)) {
case 2: return 6
case 3: return 7
default: return 5
}
}
/**
* Somme des niveaux des Adaptations/Mutations déjà possédées.
* @param {foundry.abstract.Document} actor personnage
* @returns {number}
*/
static evolutionTotalLevels(actor) {
return (actor?.itemTypes?.evolution || []).reduce((sum, item) => sum + (item.system?.level?.value || 0), 0)
}
/**
* Valide l'achat d'une Adaptation/Mutation contre les Dés d'Évolution
* accumulés. Retourne une clé i18n d'erreur, ou null si l'achat est permis.
* Règles p. 116-117 : coût = niveau ; total limité par le Mode ; Mutations
* 3D/4D interdites en Mode Survie ; une seule Mutation à 4D.
* @param {foundry.abstract.Document} actor personnage
* @param {number} cost niveau de l'Adaptation/Mutation (1-4)
* @param {boolean} mutation vrai si c'est une Mutation
* @returns {string|null}
*/
static validateEvolutionPurchase(actor, cost, mutation) {
const level = parseInt(cost, 10) || 0
const accumulated = actor.system?.experience?.evolution || 0
const mode = this.gameMode()
if (accumulated < level) return "VERMINE.error_evolution_not_enough_dice"
if (level < 1 || level > 4) return "VERMINE.error_evolution_invalid_level"
const total = this.evolutionTotalLevels(actor) + level
if (total > this.evolutionMaxForMode(mode)) return "VERMINE.error_evolution_mode_limit"
if (mutation && level >= 3 && mode < 2) return "VERMINE.error_evolution_mutation_mode"
if (mutation && level === 4) {
const hasFourDMutation = (actor?.itemTypes?.evolution || []).some(
(item) => item.system?.mutation && (item.system?.level?.value || 0) === 4
)
if (hasFourDMutation) return "VERMINE.error_evolution_mutation_4d"
}
return null
}
}