/** * DataModel pour les acteurs de type "npc" (PNJ). * Étend foundry.abstract.TypeDataModel. * * Conforme aux règles des PNJ (regles/regles_pnj.txt) : un PNJ n'a ni * caractéristiques chiffrées, ni réserves détaillées, ni liste de compétences. * Il est défini uniquement par son Profil (identité) et trois critères : * Menace, Expérience et Rôle. Toutes les valeurs utiles en jeu sont dérivées * des tableaux de référence (CONFIG.VERMINE.npc*Levels) et exposées dans * "system.computed". * * Note : le champ libre de description des Spécialités est nommé "freeSkills" * pour éviter le conflit avec le champ "skills" que le template.json historique * définissait comme texte libre pour les PNJ. */ import { woundSchema, combatStatusSchema, equipmentSchema, attributeSchema } 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 utilise "freeSkills" pour cette description. * @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é (Profil du PNJ) identity: new fields.SchemaField({ name: new fields.StringField({ required: true, nullable: false, initial: "" }), age: 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: "" }) }), // Critères de définition du PNJ (Menace, Expérience, Rôle) threat: attributeSchema(1, 1, 4), experience: attributeSchema(1, 1, 4), role: attributeSchema(1, 1, 4), // Description libre des Spécialités (champ texte PNJ) freeSkills: new fields.StringField({ required: true, nullable: false, initial: "" }), // Valeurs calculées (dérivées des niveaux de menace / expérience / rôle) computed: new fields.SchemaField({ attack: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), vigor: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), action: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3 }), specialties: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 4 }), rerolls: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), contact: new fields.StringField({ required: true, nullable: false, initial: "7" }), reaction: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3 }), reactionBonus: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), pools: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), gear: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 9 }), gearHindrance: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }), protection: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1 }) }), // Équipement equipment: equipmentSchema() } } /** @override */ prepareDerivedData() { super.prepareDerivedData() // 1. Calculer les valeurs dérivées selon les niveaux de menace/expérience/rôle this._setNpcComputedValues() // 2. Calculer les seuils de blessures selon le niveau de menace this._setNpcWoundThresholds() // 3. Mettre à jour le statut de combat this._updateCombatStatus() } /** * Calcule les valeurs dérivées à partir des niveaux de Menace, Expérience * et Rôle. Utilise CONFIG.VERMINE.npcThreatLevels, .npcExperienceLevels * et .npcRoleLevels. */ _setNpcComputedValues() { const threatLevel = this.threat?.value || 1 const expLevel = this.experience?.value || 1 const roleLevel = this.role?.value || 1 const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {} const expConfig = CONFIG.VERMINE.npcExperienceLevels[expLevel] || {} const roleConfig = CONFIG.VERMINE.npcRoleLevels[roleLevel] || {} // Menace this.computed.attack = threatConfig.attack || 0 this.computed.vigor = threatConfig.vigor || 0 // Expérience this.computed.action = expConfig.action || 3 this.computed.specialties = expConfig.specialties || 4 this.computed.rerolls = expConfig.rerolls || 0 this.computed.contact = expConfig.contact || "7" // Rôle this.computed.reaction = roleConfig.reaction || 0 this.computed.reactionBonus = roleConfig.reaction_bonus || 0 this.computed.pools = roleConfig.pools || 0 this.computed.gear = roleConfig.gear || 9 this.computed.gearHindrance = roleConfig.gear_hindrance || 0 this.computed.protection = roleConfig.protection || 1 } /** * Calcule les seuils et le nombre de boîtes de blessures à partir du * niveau de menace (tableau "Niveau de Menace"). Utilise * CONFIG.VERMINE.npcThreatLevels. */ _setNpcWoundThresholds() { const threatLevel = this.threat?.value || 1 const threatConfig = CONFIG.VERMINE.npcThreatLevels[threatLevel] || {} this.minorWound.threshold = threatConfig.minorThreshold ?? 1 this.majorWound.threshold = threatConfig.majorThreshold ?? 4 this.deadlyWound.threshold = threatConfig.deadlyThreshold ?? 6 this.minorWound.max = threatConfig.minorWound || 1 this.majorWound.max = threatConfig.majorWound || 1 this.deadlyWound.max = threatConfig.deadlyWound || 1 } /** * 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 } } }