feat: implémentation complète du système Célestopol 1922 pour FoundryVTT v13
- DataModels (character, npc, anomaly, aspect, attribute, equipment) - ApplicationV2 sheets (character 5 tabs, npc 3 tabs, 4 item sheets) - DialogV2 pour les jets de dés avec phase de lune - Templates Handlebars complets (fiches PJ/PNJ, items, jet, chat) - Styles LESS → CSS compilé (thème vert foncé / orange CopaseticNF) - i18n fr.json complet (clés CELESTOPOL.*) - Point d'entrée fvtt-celestopol.mjs avec hooks init/ready - Assets : polices CopaseticNF, images UI, icônes items - Mise à jour copilot-instructions.md avec l'architecture réelle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
175
module/models/character.mjs
Normal file
175
module/models/character.mjs
Normal file
@@ -0,0 +1,175 @@
|
||||
import { SYSTEM } from "../config/system.mjs"
|
||||
|
||||
export default class CelestopolCharacter extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const reqInt = { required: true, nullable: false, integer: true }
|
||||
const schema = {}
|
||||
|
||||
// Concept du personnage
|
||||
schema.concept = new fields.StringField({ required: true, nullable: false, initial: "" })
|
||||
|
||||
// Initiative (calculée mais stockée pour affichage)
|
||||
schema.initiative = new fields.NumberField({ ...reqInt, initial: 0, min: 0 })
|
||||
|
||||
// Anomalie du personnage
|
||||
schema.anomaly = new fields.SchemaField({
|
||||
type: new fields.StringField({ required: true, nullable: false, initial: "none",
|
||||
choices: Object.keys(SYSTEM.ANOMALY_TYPES) }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
// Les 4 stats avec leurs compétences
|
||||
const skillField = (label) => new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: label }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
const statField = (statId) => {
|
||||
const skills = SYSTEM.SKILLS[statId]
|
||||
const skillSchema = {}
|
||||
for (const [key, skill] of Object.entries(skills)) {
|
||||
skillSchema[key] = skillField(skill.label)
|
||||
}
|
||||
return new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, initial: SYSTEM.STATS[statId].label }),
|
||||
res: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
...skillSchema,
|
||||
})
|
||||
}
|
||||
|
||||
schema.stats = new fields.SchemaField({
|
||||
ame: statField("ame"),
|
||||
corps: statField("corps"),
|
||||
coeur: statField("coeur"),
|
||||
esprit: statField("esprit"),
|
||||
})
|
||||
|
||||
// Blessures (8 cases)
|
||||
const woundField = (idx) => new fields.SchemaField({
|
||||
checked: new fields.BooleanField({ required: true, initial: false }),
|
||||
malus: new fields.NumberField({ ...reqInt, initial: SYSTEM.WOUND_LEVELS[idx]?.malus ?? 0 }),
|
||||
})
|
||||
schema.blessures = new fields.SchemaField({
|
||||
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
b1: woundField(1), b2: woundField(2), b3: woundField(3), b4: woundField(4),
|
||||
b5: woundField(5), b6: woundField(6), b7: woundField(7), b8: woundField(8),
|
||||
})
|
||||
|
||||
// Destin (8 cases)
|
||||
const destField = () => new fields.SchemaField({
|
||||
checked: new fields.BooleanField({ required: true, initial: false }),
|
||||
})
|
||||
schema.destin = new fields.SchemaField({
|
||||
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
d1: destField(), d2: destField(), d3: destField(), d4: destField(),
|
||||
d5: destField(), d6: destField(), d7: destField(), d8: destField(),
|
||||
})
|
||||
|
||||
// Spleen (8 cases)
|
||||
schema.spleen = new fields.SchemaField({
|
||||
lvl: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
s1: destField(), s2: destField(), s3: destField(), s4: destField(),
|
||||
s5: destField(), s6: destField(), s7: destField(), s8: destField(),
|
||||
})
|
||||
|
||||
// Attributs de personnage (Entregent, Fortune, Rêve, Vision)
|
||||
const persoAttrField = () => new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
max: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
})
|
||||
schema.attributs = new fields.SchemaField({
|
||||
entregent: persoAttrField(),
|
||||
fortune: persoAttrField(),
|
||||
reve: persoAttrField(),
|
||||
vision: persoAttrField(),
|
||||
})
|
||||
|
||||
// Factions
|
||||
const factionField = () => new fields.SchemaField({
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
})
|
||||
schema.factions = new fields.SchemaField({
|
||||
pinkerton: factionField(),
|
||||
police: factionField(),
|
||||
okhrana: factionField(),
|
||||
lunanovatek: factionField(),
|
||||
oto: factionField(),
|
||||
syndicats: factionField(),
|
||||
vorovskoymir: factionField(),
|
||||
cour: factionField(),
|
||||
perso1: new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
}),
|
||||
perso2: new fields.SchemaField({
|
||||
label: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
||||
}),
|
||||
})
|
||||
|
||||
// Préférences de jet (mémorisé entre sessions)
|
||||
schema.prefs = new fields.SchemaField({
|
||||
moonPhase: new fields.StringField({ required: true, nullable: false, initial: "none" }),
|
||||
difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||
})
|
||||
|
||||
// Description & notes
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
|
||||
// Données biographiques
|
||||
schema.biodata = new fields.SchemaField({
|
||||
age: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
genre: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
taille: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
yeux: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
naissance: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
cheveux: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
origine: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
})
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
static LOCALIZATION_PREFIXES = ["CELESTOPOL.Character"]
|
||||
|
||||
prepareDerivedData() {
|
||||
super.prepareDerivedData()
|
||||
// L'initiative est basée sur la résistance Corps
|
||||
this.initiative = this.stats.corps.res
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le malus de blessures actif.
|
||||
* @returns {number}
|
||||
*/
|
||||
getWoundMalus() {
|
||||
const lvl = Math.max(0, Math.min(8, this.blessures.lvl))
|
||||
return SYSTEM.WOUND_LEVELS[lvl]?.malus ?? 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance les dés pour une compétence donnée.
|
||||
* @param {string} statId - Id de la stat (ame, corps, coeur, esprit)
|
||||
* @param {string} skillId - Id de la compétence
|
||||
*/
|
||||
async roll(statId, skillId) {
|
||||
const { CelestopolRoll } = await import("../documents/roll.mjs")
|
||||
const skill = this.stats[statId][skillId]
|
||||
if (!skill) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId,
|
||||
skillId,
|
||||
skillLabel: skill.label,
|
||||
skillValue: skill.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
moonPhase: this.prefs.moonPhase,
|
||||
difficulty: this.prefs.difficulty,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user