feat: fiche de Communauté (socle)

- acteur community (identité, géographie, histoire, objectifs, taille/effectif)
- 4 Domaines x 2 jauges avec états (Crise/Bas/Stable/Opulent), moral, réserves
- onglets Identité / Domaines / Ressources, édition ProseMirror (formInput)
- header avec fond, totem, niveau, effectif ; icône par défaut
- libellé du type via typeLabels (sans écraser TYPES.* du core)
This commit is contained in:
2026-08-04 17:20:29 +02:00
parent 8d8428cad3
commit 6247f65590
17 changed files with 1080 additions and 7 deletions
+195
View File
@@ -0,0 +1,195 @@
/**
* DataModel pour les acteurs de type "community" (Communauté).
* La Communauté est l'extension sédentarisée d'un Groupe : elle englobe le
* Groupe de personnages, des Figures et des Habitants répartis en Compagnies.
* Le socle couvre l'identité, la géographie, l'histoire, les objectifs, la
* taille/l'effectif, les quatre Domaines (8 jauges), le Moral et les réserves.
*/
import {
attributeSchema,
levelSchema,
combatStatusSchema
} from "./_shared.mjs"
export default class VermineCommunityData extends foundry.abstract.TypeDataModel {
/** @override */
static LOCALIZATION_PREFIXES = ["VERMINE.community"]
/** @override */
static defineSchema() {
const fields = foundry.data.fields
const reqInt = { required: true, nullable: false, integer: true }
const gauge = () => attributeSchema(0, 0, 10)
return {
// Statut de combat (base, peu utilisé pour une Communauté)
combatStatus: combatStatusSchema("9"),
// Identité
identity: new fields.SchemaField({
totem: 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: "" }),
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: "" }),
// Âge de la Communauté : recent | established | old
age: new fields.StringField({ required: true, nullable: false, initial: "recent" }),
notes: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Géographie / localisation
location: new fields.SchemaField({
sector: new fields.StringField({ required: true, nullable: false, initial: "" }),
placeType: new fields.StringField({ required: true, nullable: false, initial: "" }),
landmarks: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
territory: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Histoire
history: new fields.SchemaField({
founding: new fields.StringField({ required: true, nullable: false, initial: "" }),
events: new fields.HTMLField({ required: true, initial: "", textSearch: true }),
relations: new fields.HTMLField({ required: true, initial: "", textSearch: true })
}),
// Objectifs : un majeur + des mineurs
objectives: new fields.SchemaField({
major: new fields.StringField({ required: true, nullable: false, initial: "" }),
minor: new fields.ArrayField(new fields.StringField({ required: true, nullable: false, initial: "" }))
}),
// Taille (1-3) et effectif
size: attributeSchema(1, 1, 3),
population: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
// Domaines : 4 Domaines × 2 sections (jauges en Dés)
domains: new fields.SchemaField({
resources: new fields.SchemaField({
vivre: gauge(),
supplies: gauge()
}),
security: new fields.SchemaField({
defense: gauge(),
care: gauge()
}),
dynamism: new fields.SchemaField({
cohesion: gauge(),
productivity: gauge()
}),
relations: new fields.SchemaField({
human: gauge(),
ecology: gauge()
})
}),
// Moral (comme le Groupe) : value 0-7, level dérivé
morale: new fields.SchemaField({
level: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
value: new fields.NumberField({ ...reqInt, initial: 7, min: 0, max: 7 }),
min: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
max: new fields.NumberField({ ...reqInt, initial: 7, min: 0 })
}),
// Réserve de Groupe
reserve: attributeSchema(0, 0, 10),
// Dés collectifs de la Communauté
material: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
experience: new fields.SchemaField({
dice: new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
}),
// Niveau de la Communauté (pour l'expérience des habitants)
level: levelSchema(1, 1, 10),
// Groupe de personnages rattaché (id d'acteur)
groupId: new fields.StringField({ required: true, nullable: false, initial: "" }),
// Habitants (phase 2) : Figures et Compagnies
figures: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
role: new fields.StringField({ required: true, nullable: false, initial: "" }),
totem: new fields.StringField({ required: true, nullable: false, initial: "" }),
specialty: new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
})
})),
companies: new fields.ArrayField(new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
specialty: new fields.SchemaField({
name: new fields.StringField({ required: true, nullable: false, initial: "" }),
score: new fields.NumberField({ ...reqInt, initial: 3, min: 0, max: 10 })
}),
level: levelSchema(1, 1, 4),
members: new fields.NumberField({ ...reqInt, initial: 1, min: 0 }),
figureName: new fields.StringField({ required: true, nullable: false, initial: "" })
}))
}
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData()
// 1. État (Crise/Bas/Stable/Opulent) de chaque jauge de Domaine
this._computeGaugeStates()
// 2. Les quatre Domaines sont-ils Bas ? (Moral inactif)
this._computeAllDomainsLow()
// 3. Mettre à jour le niveau de Moral selon la valeur
this._updateMorale()
}
/**
* État d'une jauge selon sa valeur en Dés.
* Crise 0D / Bas 1-2D / Stable 3-5D / Opulent 6D+.
* @param {number} value
* @returns {string}
*/
static gaugeState(value) {
const v = parseInt(value, 10) || 0
if (v >= 6) return "opulent"
if (v >= 3) return "stable"
if (v >= 1) return "low"
return "crisis"
}
_computeGaugeStates() {
for (const domain of Object.values(this.domains)) {
for (const gauge of Object.values(domain)) {
gauge.state = VermineCommunityData.gaugeState(gauge.value)
}
}
}
/**
* Un Domaine est "Bas" lorsque ses deux sections sont à 2D ou moins.
* Si les quatre Domaines sont Bas, le Bonus de Moral est inactif.
*/
_computeAllDomainsLow() {
const domainLow = (domain) => Object.values(domain).every(g => (parseInt(g.value, 10) || 0) <= 2)
this.allDomainsLow = Object.values(this.domains).every(domainLow)
}
/**
* Met à jour le niveau de Moral en fonction de la valeur de dés.
* Règles : 7D = Haut, 6-3D = Normal, 2-1D = Bas, 0D = Crise.
*/
_updateMorale() {
const moraleValue = parseInt(this.morale?.value, 10) || 0
if (this.morale.level && this.morale.level !== "normal") return
if (moraleValue >= 7) {
this.morale.level = "high"
} else if (moraleValue >= 3) {
this.morale.level = "normal"
} else if (moraleValue >= 1) {
this.morale.level = "low"
} else {
this.morale.level = "crisis"
}
}
}