Files
vermine2047/module/models/character.mjs
T
uberwald 9cf7d9ac68
Release Creation / build (release) Failing after 1m20s
feat: système d'expérience et transferts de dés d'expérience
- phase d'expérience (récompenses collectives et individuelles, objectifs, traumatisme)
- jets d'apprentissage (compétences, spécialités, caractéristiques, réserves, évolution)
- bonus de réserve acquis par apprentissage intégré au max dérivé
- transferts de dés individuels <-> groupe dans le dialogue de jet
- onglet expérience sur la fiche personnage
- guide utilisateur combat (docs/user/COMBAT.md)
2026-08-04 14:14:09 +02:00

233 lines
9.7 KiB
JavaScript

/**
* DataModel pour les acteurs de type "character" (personnage).
* Étend foundry.abstract.TypeDataModel.
*/
import {
woundSchema,
combatStatusSchema,
equipmentSchema,
attributeSchema,
abilitiesSchema,
skillCategoriesSchema,
skillsSchema
} from "./_shared.mjs"
export default class VermineCharacterData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.character"]
/** @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)
combatStatus: combatStatusSchema("7"),
// Adaptation (totems humain/adapté)
adaptation: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 5 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 5, min: 0 }),
totems: new fields.SchemaField({
human: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 3 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3, min: 0 })
}),
adapted: new fields.SchemaField({
value: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 1, min: 0, max: 3 }),
min: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
max: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 3, min: 0 })
})
})
}),
// Identité
identity: new fields.SchemaField({
height: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
weight: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0 }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
age: new fields.StringField({ required: true, nullable: false, initial: "15" }),
ageType: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 2 }),
profile: new fields.StringField({ required: true, nullable: false, initial: "" }),
theme: new fields.StringField({ required: true, nullable: false, initial: "" }),
instincts: new fields.StringField({ required: true, nullable: false, initial: "" }),
prohibits: new fields.StringField({ required: true, nullable: false, initial: "" }),
objectives: new fields.StringField({ required: true, nullable: false, initial: "" }),
relations: new fields.HTMLField({ required: true, initial: "" }),
biography: new fields.HTMLField({ required: true, initial: "" })
}),
// Équipement
equipment: equipmentSchema(),
// 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, 10),
effort: attributeSchema(0, 0, 10)
}),
// Expérience (dés, récompenses, évolution, apprentissage)
experience: new fields.SchemaField({
dice: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
collective: new fields.SchemaField({
danger: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
discoveries: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
duration: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
}),
individual: new fields.SchemaField({
implication: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
influence: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 }),
interpretation: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0, max: 3 })
}),
evolution: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
traumaReward: new fields.BooleanField({ required: true, initial: false }),
// Bonus de Réserve acquis par apprentissage (s'ajoute au max dérivé)
reserveBonus: new fields.SchemaField({
self_control: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
effort: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 })
}),
// Limites d'apprentissage par phase
learned: new fields.SchemaField({
phase: new fields.NumberField({ required: true, nullable: false, integer: true, initial: 0, min: 0 }),
skill: new fields.BooleanField({ required: true, initial: false }),
specialty: new fields.BooleanField({ required: true, initial: false }),
reserve: new fields.BooleanField({ required: true, initial: false }),
characteristic: new fields.BooleanField({ required: true, initial: false }),
evolution: new fields.BooleanField({ required: true, initial: false })
})
}),
// Rencontres
encounters: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" })),
// Caractéristiques (8)
abilities: abilitiesSchema(),
// Catégories de compétences
skill_categories: skillCategoriesSchema(),
// Compétences (30)
skills: skillsSchema()
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
this._setAgeType()
this._setSelfControlMax()
this._setEffortMax()
this._setWoundThresholds()
this._updateCombatStatus()
}
/**
* Détermine la tranche d'âge (1=jeune, 2=adulte, 3=vieux)
* à partir de l'âge et de la config VERMINE.AgeTypes.
*/
_setAgeType() {
const age = this.identity.age
const ageTypes = CONFIG.VERMINE.AgeTypes
const entries = Object.entries(ageTypes).reverse()
for (const [type, cfg] of entries) {
if (age >= parseInt(cfg.beginning, 10)) {
this.identity.ageType = parseInt(type, 10)
break
}
}
}
/**
* Calcule le max de sang-froid :
* somme des caractéristiques mentales + sociales + modificateur d'âge.
*/
_setSelfControlMax() {
const abilities = Object.values(this.abilities)
const modFromAge = this._getModFromAgeSelfControl()
const sum = abilities
.filter(a => a.category === "mental" || a.category === "social")
.reduce((acc, a) => acc + a.value, 0)
const bonus = this.experience?.reserveBonus?.self_control || 0
this.attributes.self_control.max = Math.min(10, sum + modFromAge + bonus)
}
/**
* Calcule le max d'effort :
* somme des caractéristiques physiques + manuelles + modificateur d'âge.
*/
_setEffortMax() {
const abilities = Object.values(this.abilities)
const modFromAge = this._getModFromAgeEffort()
const sum = abilities
.filter(a => a.category === "physical" || a.category === "manual")
.reduce((acc, a) => acc + a.value, 0)
const bonus = this.experience?.reserveBonus?.effort || 0
this.attributes.effort.max = Math.min(10, sum + modFromAge + bonus)
}
/**
* Calcule les seuils de blessures à partir de la Santé.
*/
_setWoundThresholds() {
const health = this.abilities.health.value
const ageMods = this._getModFromAgeWounds()
this.minorWound.threshold = health
this.majorWound.threshold = health + 3
this.deadlyWound.threshold = (health + 7 < 11) ? health + 7 : 10
this.minorWound.max = 4 + ageMods.l
this.majorWound.max = 3 + ageMods.h
this.deadlyWound.max = 2 + ageMods.d
}
/**
* 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
}
}
// ── Modificateurs liés à l'âge ──────────────────────────────────────
/** @returns {number} Modificateur de sang-froid selon l'âge. */
_getModFromAgeSelfControl() {
return this.identity.ageType === 1 ? -1 : 0
}
/** @returns {number} Modificateur d'effort selon l'âge. */
_getModFromAgeEffort() {
if (this.identity.ageType === 1) return -1
if (this.identity.ageType === 3) return -2
return 0
}
/** @returns {{l: number, h: number, d: number}} Modificateurs de blessures selon l'âge. */
_getModFromAgeWounds() {
if (this.identity.ageType === 1) return { l: 0, h: 0, d: -1 }
if (this.identity.ageType === 3) return { l: -1, h: -1, d: -1 }
return { l: 0, h: 0, d: 0 }
}
}