Release Creation / build (release) Failing after 1m32s
- 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.
295 lines
9.3 KiB
JavaScript
295 lines
9.3 KiB
JavaScript
/**
|
||
* 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
|
||
}
|
||
}
|