/** * 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} */ 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 } }