/** * DataModel pour les acteurs de type "npc" (PNJ). * Étend foundry.abstract.TypeDataModel. * * Note : le champ libre de compétences (texte descriptif) est nommé "freeSkills" * pour éviter le conflit avec le SchemaField "skills" qui contient les 30 compétences. */ import { woundSchema, combatStatusSchema, equipmentSchema, attributeSchema, abilitiesSchema, skillCategoriesSchema, skillsSchema } from "./_shared.mjs" export default class VermineNpcData extends foundry.abstract.TypeDataModel { /** @override */ static LOCALIZATION_PREFIXES = ["VERMINE.npc"] /** * Migration des données avant traitement par le schéma. * Avant DataModel, template.json définissait "skills" comme un champ texte libre * pour les PNJ. Le DataModel réserve "skills" pour les 30 compétences individuelles * (SchemaField) et utilise "freeSkills" pour le texte libre. * @param {Object} source Données brutes avant validation du schéma * @returns {Object} Données migrées */ static migrateData(source) { if (typeof source.skills === "string") { source.freeSkills = source.skills } return super.migrateData(source) } /** @override */ static defineSchema() { const fields = foundry.data.fields return { // Blessures (base) minorWound: new fields.SchemaField(woundSchema(1, 5)), majorWound: new fields.SchemaField(woundSchema(4, 4)), deadlyWound: new fields.SchemaField(woundSchema(8, 2)), // Statut de combat (base, difficulté par défaut 9 pour PNJ) combatStatus: combatStatusSchema("9"), // Identité identity: new fields.SchemaField({ name: new fields.StringField({ required: true, nullable: false, initial: "" }), profile: new fields.StringField({ required: true, nullable: false, initial: "" }), origin: new fields.StringField({ required: true, nullable: false, initial: "" }), totem: new fields.StringField({ required: true, nullable: false, initial: "" }), theme: new fields.StringField({ required: true, nullable: false, initial: "" }), notes: new fields.HTMLField({ required: true, initial: "" }) }), // Attributs (XP, réputation, sang-froid, effort) attributes: new fields.SchemaField({ xp: attributeSchema(0, 0, 10), reputation: attributeSchema(0, 0, 10), self_control: attributeSchema(0, 0, 5), effort: attributeSchema(0, 0, 5) }), // Niveaux PNJ (menace, expérience, rôle) threat: attributeSchema(1, 1, 4), experience: attributeSchema(1, 1, 4), role: attributeSchema(1, 1, 4), // Compétences (les 30 compétences individuelles) skills: skillsSchema(), // Description libre des compétences (champ texte PNJ) freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }), // Catégories de compétences skill_categories: skillCategoriesSchema(), // Caractéristiques (8) abilities: abilitiesSchema(), // Équipement equipment: equipmentSchema() } } /** @override */ prepareDerivedData() { super.prepareDerivedData() // 1. Calculer les seuils de blessures selon le niveau de menace this._setNpcWoundThresholds() // 2. Calculer les réserves selon le niveau de rôle this._setNpcAttributes() // 3. Définir les libellés des caractéristiques this._setAbilityLabels() // 4. Mettre à jour le statut de combat this._updateCombatStatus() } /** * Calcule les seuils de blessures à partir du niveau de menace. * Utilise CONFIG.VERMINE.npcThreatLevels. */ _setNpcWoundThresholds() { const health = this.abilities?.health?.value || 1 const threatLevel = this.threat?.value || 1 const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {} this.minorWound.threshold = threatConfig.minorWound || health this.majorWound.threshold = threatConfig.majorWound || (health + 3) this.deadlyWound.threshold = threatConfig.deadlyWound || (health + 7 < 11 ? health + 7 : 10) this.minorWound.max = threatConfig.minorWound || 4 this.majorWound.max = threatConfig.majorWound || 3 this.deadlyWound.max = threatConfig.deadlyWound || 2 } /** * Définit les attributs dérivés (effort, sang-froid) selon le niveau de rôle. * Utilise CONFIG.VERMINE.npcRoleLevels. */ _setNpcAttributes() { const roleLevel = this.role?.value || 1 const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {} this.attributes.effort.max = roleConfig.pools || 0 this.attributes.self_control.max = roleConfig.reaction_bonus || 0 } /** * Définit les libellés localisés des caractéristiques. */ _setAbilityLabels() { for (const [k, v] of Object.entries(this.abilities)) { v.label = game.i18n.localize(CONFIG.VERMINE.abilities[k]) ?? k } } /** * Met à jour le label du statut de combat en fonction de la difficulté. */ _updateCombatStatus() { const difficulty = parseInt(this.combatStatus.difficulty) || 9 let newLabel = "Passif" switch (difficulty) { case 5: newLabel = "Offensif"; break case 7: newLabel = "Actif"; break case 9: newLabel = "Passif"; break } if (this.combatStatus.label !== newLabel) { this.combatStatus.label = newLabel } } }