Tâche 0 : - item.mjs : champ equipped sur items génériques + toggle fiche - nouveau module/system/traits.mjs (helpers partagés : equippedItems, lourdMalus, mobilityHandicap, feticheItem…) Phase 2 — Combat : - Lourd : malus cumulés auto (Vigueur < n) dans le dialogue + pool - Rapide : handicap (n) pré-rempli dans la défense (character + PNJ) + note carte - Rafale : case tir en rafale (+2 difficulté), +n réussites si réussi (bonusOnSuccess) - Portée : portée effective n × Vigueur affichée - Maniable/Mobilité : note de rappel (dialogue + carte) - Incapacitant : bouton « Jet de Santé (n) » sur la carte d'échange - getLabel() : libellés caractéristiques/compétences localisés Phase 3 — Jets d'action : - Ponctuel : annule jusqu'à n Handicaps, consommé au jet (Traits supprimé à 0) - Pratique : outil sélectionné +2D au lieu de +1D - Intimidant : case « Brandit » → -1D Psychologie ou Relance 1D - Malus : champ « Malus temporaires » manuel déduit du pool - Durée : note « n tours » sur la carte de jet d'arme - roll() : transmission effectiveRange/mobilityHandicap/ponctuelUsed/intimidant (corrige l'affichage Portée/Mobilité) Phase 4 — Acteur : - Fétiche : +1D Effort ET Sang-Froid (max dérivé, config corrigé), alerte 2+ fétiches, action « Perdre le fétiche » (-2D, max -1D persistant feticheLoss) - Zone : note + bouton « Appliquer aux cibles de la zone » (#onApplyZone) CSS : styles LESS pour les nouvelles classes (fetiche-block, exchange-reminder/zone, traits-reminder, tool-bonus/ponctuel, duree-note) i18n : 29 clés fr/en TRAITS_PLAN.md : plan complet cochée
250 lines
11 KiB
JavaScript
250 lines
11 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"
|
|
import { VermineTraits } from "../system/traits.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: "" }),
|
|
// Statut de Chaman : débloque l'affichage des Rites chamaniques maîtrisés.
|
|
chaman: new fields.BooleanField({ required: true, initial: false }),
|
|
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 })
|
|
}),
|
|
// Perte de Réserves après perte du Fétiche : -1D de max persistant par Réserve
|
|
feticheLoss: 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 }),
|
|
ability: new fields.BooleanField({ required: true, initial: false }),
|
|
background: new fields.BooleanField({ required: true, initial: false }),
|
|
reputation: 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.
|
|
* Un Fétiche équipé ajoute +1D ; une perte de Fétiche soustrait 1D.
|
|
*/
|
|
_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
|
|
const loss = this.experience?.feticheLoss?.self_control || 0
|
|
const fetiche = VermineTraits.feticheItem(this.parent) ? 1 : 0
|
|
this.attributes.self_control.max = Math.min(10, Math.max(0, sum + modFromAge + bonus + fetiche - loss))
|
|
}
|
|
|
|
/**
|
|
* Calcule le max d'effort :
|
|
* somme des caractéristiques physiques + manuelles + modificateur d'âge.
|
|
* Un Fétiche équipé ajoute +1D ; une perte de Fétiche soustrait 1D.
|
|
*/
|
|
_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
|
|
const loss = this.experience?.feticheLoss?.effort || 0
|
|
const fetiche = VermineTraits.feticheItem(this.parent) ? 1 : 0
|
|
this.attributes.effort.max = Math.min(10, Math.max(0, sum + modFromAge + bonus + fetiche - loss))
|
|
}
|
|
|
|
/**
|
|
* 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 }
|
|
}
|
|
}
|