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:
2026-03-31 00:33:59 +02:00
parent 79a68ee9ab
commit 9dbd614c5a
40 changed files with 849 additions and 529 deletions

View File

@@ -42,6 +42,7 @@ export class CelestopolRoll extends Roll {
const fortuneValue = options.fortuneValue ?? 0
const isResistance = options.isResistance ?? false
const isCombat = options.isCombat ?? false
const isRangedDefense = options.isRangedDefense ?? false
const weaponType = options.weaponType ?? "melee"
const weaponName = options.weaponName ?? null
const weaponDegats = options.weaponDegats ?? "0"
@@ -72,6 +73,7 @@ export class CelestopolRoll extends Roll {
defaultRollMoonDie: options.rollMoonDie ?? false,
isResistance,
isCombat,
isRangedDefense,
weaponType,
weaponName,
weaponDegats,
@@ -221,6 +223,7 @@ export class CelestopolRoll extends Roll {
autoSuccess,
isResistance,
isCombat,
isRangedDefense,
weaponType,
weaponName,
weaponDegats,
@@ -243,66 +246,49 @@ export class CelestopolRoll extends Roll {
// Test de résistance échoué → cocher automatiquement la prochaine case de blessure
const actor = game.actors.get(options.actorId)
if (isResistance && actor && roll.options.resultType === "failure") {
const wounds = actor.system.blessures
const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked)
if (nextIdx) {
await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true })
roll.options.woundTaken = nextIdx
const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
if (nextLvl <= 8) {
await actor.update({ "system.blessures.lvl": nextLvl })
roll.options.woundTaken = nextLvl
}
}
// Combat mêlée échoué → joueur prend une blessure
if (isCombat && weaponType === "melee" && actor && roll.options.resultType === "failure") {
const wounds = actor.system.blessures
const nextIdx = [1,2,3,4,5,6,7,8].find(i => !wounds[`b${i}`]?.checked)
if (nextIdx) {
await actor.update({ [`system.blessures.b${nextIdx}.checked`]: true })
roll.options.woundTaken = nextIdx
// Mêlée échouée OU défense à distance échouée → joueur prend une blessure
if (isCombat && (weaponType === "melee" || isRangedDefense) && actor && roll.options.resultType === "failure") {
const nextLvl = (actor.system.blessures.lvl ?? 0) + 1
if (nextLvl <= 8) {
await actor.update({ "system.blessures.lvl": nextLvl })
roll.options.woundTaken = nextLvl
}
}
await roll.toMessage({}, { rollMode: rollData.rollMode })
// Destin utilisé → vider la jauge (reset à 0)
if (rollData.useDestin && actor) {
await actor.update({
"system.destin.lvl": 0,
"system.destin.d1.checked": false,
"system.destin.d2.checked": false,
"system.destin.d3.checked": false,
"system.destin.d4.checked": false,
"system.destin.d5.checked": false,
"system.destin.d6.checked": false,
"system.destin.d7.checked": false,
"system.destin.d8.checked": false,
})
}
// Fortune utilisée → décrémenter de 1 (min 0)
if (rollData.useFortune && actor) {
const currentFortune = actor.system.attributs.fortune.value ?? 0
await actor.update({ "system.attributs.fortune.value": Math.max(0, currentFortune - 1) })
}
// Puiser dans ses ressources → coche une case de spleen
if (rollData.puiserRessources && actor) {
const currentSpleen = actor.system.spleen.lvl ?? 0
if (currentSpleen < 8) {
const newLvl = currentSpleen + 1
const key = `s${newLvl}`
await actor.update({
"system.spleen.lvl": newLvl,
[`system.spleen.${key}.checked`]: true,
})
}
}
// Mémoriser les préférences sur l'acteur
// Batching de toutes les mises à jour de l'acteur en un seul appel réseau
if (actor) {
await actor.update({
"system.prefs.rollMoonDie": rollData.rollMoonDie,
"system.prefs.difficulty": difficulty,
})
const updateData = {}
if (rollData.useDestin) {
updateData["system.destin.lvl"] = 0
}
if (rollData.useFortune) {
const currentFortune = actor.system.attributs.fortune.value ?? 0
updateData["system.attributs.fortune.value"] = Math.max(0, currentFortune - 1)
}
if (rollData.puiserRessources) {
const currentSpleen = actor.system.spleen.lvl ?? 0
if (currentSpleen < 8) {
updateData["system.spleen.lvl"] = currentSpleen + 1
}
}
// Mémoriser les préférences
updateData["system.prefs.rollMoonDie"] = rollData.rollMoonDie
updateData["system.prefs.difficulty"] = difficulty
await actor.update(updateData)
}
return roll
@@ -421,6 +407,7 @@ export class CelestopolRoll extends Roll {
weaponName: this.options.weaponName ?? null,
weaponDegats: this.options.weaponDegats ?? null,
weaponType: this.options.weaponType ?? null,
isRangedDefense: this.options.isRangedDefense ?? false,
woundTaken: this.options.woundTaken ?? null,
// Dé de lune
hasMoonDie: moonDieResult !== null,