feat: gestion de l'expérience (XP)
- Schéma xp dans CelestopolCharacter : actuel (éditable), log[] ({montant, raison, date}), depense (calculé dans prepareDerivedData)
- Bouton 'Dépenser XP' → DialogV2 (montant + raison) : décrémente actuel, logge l'entrée
- Suppression d'entrée de log avec remboursement des points (mode édition)
- Section XP en haut de l'onglet Biographie : compteurs, tableau du log, référentiel des coûts
- i18n : section CELESTOPOL.XP.* complète
- CSS : .xp-section avec compteurs, tableau de log et accordéon de référence
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -21,18 +21,10 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 8 }),
|
||||
})
|
||||
|
||||
// Les 4 stats avec leurs domaines
|
||||
// Les 4 stats avec leurs domaines — niveau stocké directement comme entier
|
||||
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) => {
|
||||
@@ -55,32 +47,19 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
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 }),
|
||||
})
|
||||
// Blessures — niveau entier direct (0 = aucune, 8 = fatale)
|
||||
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 }),
|
||||
})
|
||||
// Destin — jauge entière directe
|
||||
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)
|
||||
// Spleen — jauge entière directe
|
||||
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)
|
||||
@@ -95,18 +74,9 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
vision: persoAttrField(),
|
||||
})
|
||||
|
||||
// Factions - 9 checkboxes per faction (like wound tracks)
|
||||
// Factions — score entier direct (0-9)
|
||||
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 }),
|
||||
value: new fields.NumberField({ ...reqInt, initial: 0, min: 0, max: 9 }),
|
||||
})
|
||||
schema.factions = new fields.SchemaField({
|
||||
pinkerton: factionField(),
|
||||
@@ -133,6 +103,16 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
difficulty: new fields.StringField({ required: true, nullable: false, initial: "normal" }),
|
||||
})
|
||||
|
||||
// Expérience
|
||||
schema.xp = new fields.SchemaField({
|
||||
actuel: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
log: new fields.ArrayField(new fields.SchemaField({
|
||||
montant: new fields.NumberField({ ...reqInt, initial: 0, min: 0 }),
|
||||
raison: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
date: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
})),
|
||||
})
|
||||
|
||||
// Description & notes
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.notes = new fields.HTMLField({ required: true, textSearch: true })
|
||||
@@ -156,15 +136,7 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
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 Résistance par stat = +2 par domaine atteignant son seuil
|
||||
// Résistance par stat = +2 par domaine atteignant son seuil de spécialisation
|
||||
for (const [statId, statData] of Object.entries(this.stats)) {
|
||||
let res = 0
|
||||
for (const [skillId, skill] of Object.entries(statData)) {
|
||||
@@ -175,19 +147,11 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
statData.res = res
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Calcul automatique du niveau des jauges depuis les cases cochées
|
||||
this.blessures.lvl = [1,2,3,4,5,6,7,8].filter(i => this.blessures[`b${i}`]?.checked).length
|
||||
this.destin.lvl = [1,2,3,4,5,6,7,8].filter(i => this.destin[`d${i}`]?.checked).length
|
||||
this.spleen.lvl = [1,2,3,4,5,6,7,8].filter(i => this.spleen[`s${i}`]?.checked).length
|
||||
|
||||
// Initiative PJ : 4 + Mobilité (Corps) + Inspiration (Cœur) [après calcul des domaines]
|
||||
// Initiative PJ : 4 + Mobilité (Corps) + Inspiration (Cœur)
|
||||
this.initiative = 4 + (this.stats.corps.mobilite?.value ?? 0) + (this.stats.coeur.inspiration?.value ?? 0)
|
||||
|
||||
// XP dépensée = somme des montants du log
|
||||
this.xp.depense = this.xp.log.reduce((sum, entry) => sum + entry.montant, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,10 +203,29 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
const statData = this.stats[statId]
|
||||
if (!statData) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId,
|
||||
skillId: null,
|
||||
statLabel: SYSTEM.STATS[statId]?.label,
|
||||
skillLabel: "CELESTOPOL.Roll.resistanceTest",
|
||||
skillValue: statData.res,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
isResistance: true,
|
||||
rollMoonDie: false,
|
||||
destGaugeFull: false,
|
||||
fortuneValue: 0,
|
||||
difficulty: "normal",
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance une attaque avec une arme (test Échauffourée vs Corps PNJ).
|
||||
* Mêlée : échec → blessure joueur auto-cochée.
|
||||
* Distance : échec → simple raté, pas de blessure joueur.
|
||||
* Lance une attaque avec une arme.
|
||||
* Mêlée : test Échauffourée vs Corps PNJ ; échec → blessure joueur.
|
||||
* Distance : test Échauffourée vs Corps PNJ ; échec → pas de blessure joueur.
|
||||
* Égalité (marge=0) → personne n'est blessé.
|
||||
* @param {string} itemId - Id de l'item arme
|
||||
*/
|
||||
@@ -255,23 +238,60 @@ export default class CelestopolCharacter extends foundry.abstract.TypeDataModel
|
||||
if (!echauffouree) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "echauffouree",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
|
||||
skillValue: echauffouree.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
weaponType: item.system.type,
|
||||
weaponName: item.name,
|
||||
weaponDegats: item.system.degats,
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "echauffouree",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.echauffouree.label,
|
||||
skillValue: echauffouree.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
isRangedDefense: false,
|
||||
weaponType: item.system.type,
|
||||
weaponName: item.name,
|
||||
weaponDegats: item.system.degats,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lance un jet de défense contre une attaque à distance (test Mobilité vs Corps PNJ).
|
||||
* Succès → esquive réussie.
|
||||
* Échec → blessure automatique (le PNJ touche).
|
||||
* @param {string} itemId - Id de l'item arme (distance uniquement)
|
||||
*/
|
||||
async rollRangedDefense(itemId) {
|
||||
const { CelestopolRoll } = await import("../documents/roll.mjs")
|
||||
const item = this.parent.items.get(itemId)
|
||||
if (!item || item.type !== "weapon" || item.system.type !== "distance") return null
|
||||
|
||||
const mobilite = this.stats.corps.mobilite
|
||||
if (!mobilite) return null
|
||||
|
||||
return CelestopolRoll.prompt({
|
||||
actorId: this.parent.id,
|
||||
actorName: this.parent.name,
|
||||
actorImage: this.parent.img,
|
||||
statId: "corps",
|
||||
skillId: "mobilite",
|
||||
statLabel: SYSTEM.STATS.corps.label,
|
||||
skillLabel: SYSTEM.SKILLS.corps.mobilite.label,
|
||||
skillValue: mobilite.value,
|
||||
woundMalus: this.getWoundMalus(),
|
||||
woundLevel: this.blessures.lvl,
|
||||
rollMoonDie: this.prefs.rollMoonDie ?? false,
|
||||
destGaugeFull: this.destin.lvl > 0,
|
||||
fortuneValue: this.attributs.fortune.value,
|
||||
isCombat: true,
|
||||
isRangedDefense: true,
|
||||
weaponType: "distance",
|
||||
weaponName: item.name,
|
||||
weaponDegats: "0",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,14 +41,8 @@ export default class CelestopolNPC extends foundry.abstract.TypeDataModel {
|
||||
esprit: statField("esprit"),
|
||||
})
|
||||
|
||||
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),
|
||||
})
|
||||
|
||||
schema.prefs = new fields.SchemaField({
|
||||
|
||||
Reference in New Issue
Block a user