Règle : si Fortune > 0, le joueur peut cocher 'Fortune' pour lancer 1d8 + 8 au lieu de 2d8 (contraint les probabilités vers le haut). - character.mjs : passe fortuneValue dans roll.prompt() - roll.mjs : checkbox Fortune, formule 1d8 + 8 + totalModifier si cochée - roll-dialog.hbs : bloc Fortune (visible si fortuneValue > 0), preview mis à jour - chat-message.hbs : affiche '+8' dans la zone dés, ligne formule avec badge Fortune - roll.less : styles .form-fortune-row, .fl-mod.fortune, .fortune-fixed-badge - lang/fr.json : Roll.fortune/fortuneBonus/fortuneFixed/usedFortune Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
214 lines
9.1 KiB
JavaScript
214 lines
9.1 KiB
JavaScript
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: "" })
|
|
schema.metier = new fields.StringField({ required: true, nullable: false, initial: "" })
|
|
schema.faction = 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 domaines
|
|
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 }),
|
|
level1: new fields.BooleanField({ required: true, initial: false }),
|
|
level2: new fields.BooleanField({ required: true, initial: false }),
|
|
level3: new fields.BooleanField({ required: true, initial: false }),
|
|
level4: new fields.BooleanField({ required: true, initial: false }),
|
|
level5: new fields.BooleanField({ required: true, initial: false }),
|
|
level6: new fields.BooleanField({ required: true, initial: false }),
|
|
level7: new fields.BooleanField({ required: true, initial: false }),
|
|
level8: new fields.BooleanField({ required: true, initial: false }),
|
|
})
|
|
|
|
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 - 9 checkboxes per faction (like wound tracks)
|
|
const factionField = () => new fields.SchemaField({
|
|
value: new fields.NumberField({ ...reqInt, initial: 0 }),
|
|
level1: new fields.BooleanField({ required: true, initial: false }),
|
|
level2: new fields.BooleanField({ required: true, initial: false }),
|
|
level3: new fields.BooleanField({ required: true, initial: false }),
|
|
level4: new fields.BooleanField({ required: true, initial: false }),
|
|
level5: new fields.BooleanField({ required: true, initial: false }),
|
|
level6: new fields.BooleanField({ required: true, initial: false }),
|
|
level7: new fields.BooleanField({ required: true, initial: false }),
|
|
level8: new fields.BooleanField({ required: true, initial: false }),
|
|
level9: new fields.BooleanField({ required: true, initial: false }),
|
|
})
|
|
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({
|
|
rollMoonDie: new fields.BooleanField({ required: true, initial: false }),
|
|
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()
|
|
|
|
// Calcul automatique de la valeur de chaque domaine = nombre de cases cochées
|
|
for (const stat of Object.values(this.stats)) {
|
|
for (const skill of Object.values(stat)) {
|
|
if (typeof skill !== "object" || !("level1" in skill)) continue
|
|
skill.value = [1,2,3,4,5,6,7,8].filter(i => skill[`level${i}`]).length
|
|
}
|
|
}
|
|
|
|
// Calcul automatique de la valeur de chaque faction = nombre de cases cochées
|
|
for (const faction of Object.values(this.factions)) {
|
|
if (typeof faction !== "object" || !("level1" in faction)) continue
|
|
faction.value = [1,2,3,4,5,6,7,8,9].filter(i => faction[`level${i}`]).length
|
|
}
|
|
|
|
// Initiative PJ : 4 + Mobilité (Corps) + Inspiration (Cœur) [après calcul des domaines]
|
|
this.initiative = 4 + (this.stats.corps.mobilite?.value ?? 0) + (this.stats.coeur.inspiration?.value ?? 0)
|
|
}
|
|
|
|
/**
|
|
* 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 un domaine donné.
|
|
* @param {string} statId - Id de la stat (ame, corps, coeur, esprit)
|
|
* @param {string} skillId - Id du domaine
|
|
*/
|
|
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,
|
|
statLabel: SYSTEM.STATS[statId]?.label,
|
|
skillLabel: skill.label,
|
|
skillValue: skill.value,
|
|
woundMalus: this.getWoundMalus(),
|
|
woundLevel: this.blessures.lvl,
|
|
difficulty: this.prefs.difficulty,
|
|
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
|
destGaugeFull: this.destin.lvl >= 8,
|
|
fortuneValue: this.attributs.fortune.value,
|
|
})
|
|
}
|
|
}
|