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:
@@ -68,7 +68,7 @@ Hooks.once("init", () => {
|
||||
|
||||
// ── Sheets: unregister core, register system sheets ─────────────────────
|
||||
foundry.applications.sheets.ActorSheetV2.unregisterSheet?.("core", "Actor", { types: ["character", "npc"] })
|
||||
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
|
||||
foundry.appv1?.sheets?.ActorSheet && foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
|
||||
foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, CelestopolCharacterSheet, {
|
||||
types: ["character"],
|
||||
makeDefault: true,
|
||||
@@ -80,7 +80,7 @@ Hooks.once("init", () => {
|
||||
label: "CELESTOPOL.Sheet.npc",
|
||||
})
|
||||
|
||||
foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
|
||||
foundry.appv1?.sheets?.ItemSheet && foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
|
||||
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolAnomalySheet, {
|
||||
types: ["anomaly"],
|
||||
makeDefault: true,
|
||||
@@ -130,6 +130,7 @@ Hooks.once("ready", () => {
|
||||
// Migration : supprime les items de types obsolètes (ex: "attribute")
|
||||
if (game.user.isGM) {
|
||||
_migrateObsoleteItems()
|
||||
_migrateIntegerTracks()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -156,6 +157,68 @@ async function _migrateObsoleteItems() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration : convertit les anciennes données booléennes (level1..level8, b1..b8, etc.)
|
||||
* vers le nouveau stockage entier direct.
|
||||
* Ne s'applique qu'aux acteurs ayant encore l'ancien format dans leur source.
|
||||
*/
|
||||
async function _migrateIntegerTracks() {
|
||||
const validActors = game.actors.contents.filter(a => ["character", "npc"].includes(a.type))
|
||||
|
||||
for (const actor of validActors) {
|
||||
const src = actor._source?.system
|
||||
if (!src) continue
|
||||
|
||||
const updateData = {}
|
||||
|
||||
// Blessures : si b1 existe dans la source, recalculer lvl depuis les booléens
|
||||
const blessures = src.blessures ?? {}
|
||||
if ("b1" in blessures) {
|
||||
const lvl = [1,2,3,4,5,6,7,8].filter(i => blessures[`b${i}`]?.checked === true).length
|
||||
updateData["system.blessures.lvl"] = lvl
|
||||
}
|
||||
|
||||
if (actor.type === "character") {
|
||||
// Destin
|
||||
const destin = src.destin ?? {}
|
||||
if ("d1" in destin) {
|
||||
const lvl = [1,2,3,4,5,6,7,8].filter(i => destin[`d${i}`]?.checked === true).length
|
||||
updateData["system.destin.lvl"] = lvl
|
||||
}
|
||||
|
||||
// Spleen
|
||||
const spleen = src.spleen ?? {}
|
||||
if ("s1" in spleen) {
|
||||
const lvl = [1,2,3,4,5,6,7,8].filter(i => spleen[`s${i}`]?.checked === true).length
|
||||
updateData["system.spleen.lvl"] = lvl
|
||||
}
|
||||
|
||||
// Domaines : si level1 existe dans un domaine, recalculer value depuis les booléens
|
||||
const stats = src.stats ?? {}
|
||||
for (const [statId, statData] of Object.entries(stats)) {
|
||||
for (const [skillId, skill] of Object.entries(statData ?? {})) {
|
||||
if (typeof skill !== "object" || !("level1" in skill)) continue
|
||||
const value = [1,2,3,4,5,6,7,8].filter(i => skill[`level${i}`] === true).length
|
||||
updateData[`system.stats.${statId}.${skillId}.value`] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Factions : si level1 existe dans une faction, recalculer value depuis les booléens
|
||||
const factions = src.factions ?? {}
|
||||
for (const [factionId, faction] of Object.entries(factions)) {
|
||||
if (typeof faction !== "object" || !("level1" in faction)) continue
|
||||
const value = [1,2,3,4,5,6,7,8,9].filter(i => faction[`level${i}`] === true).length
|
||||
updateData[`system.factions.${factionId}.value`] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
console.log(`${SYSTEM_ID} | Migration tracks → entiers : ${actor.name}`, updateData)
|
||||
await actor.update(updateData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Handlebars helpers ─────────────────────────────────────────────────── */
|
||||
|
||||
function _registerHandlebarsHelpers() {
|
||||
@@ -183,6 +246,9 @@ function _registerHandlebarsHelpers() {
|
||||
// Helper : build array from args (Handlebars doesn't have arrays natively)
|
||||
Handlebars.registerHelper("array", (...args) => args.slice(0, -1))
|
||||
|
||||
// Helper : range(n) → [1, 2, ..., n] — pour les boucles de cases à cocher
|
||||
Handlebars.registerHelper("range", (n) => Array.from({ length: n }, (_, i) => i + 1))
|
||||
|
||||
// Helper : nested object lookup with dot path or multiple keys
|
||||
Handlebars.registerHelper("lookup", (obj, ...args) => {
|
||||
const options = args.pop() // last arg is Handlebars options hash
|
||||
|
||||
Reference in New Issue
Block a user