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:
@@ -15,6 +15,8 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
createArmure: CelestopolCharacterSheet.#onCreateArmure,
|
||||
useAnomaly: CelestopolCharacterSheet.#onUseAnomaly,
|
||||
resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses,
|
||||
depenseXp: CelestopolCharacterSheet.#onDepenseXp,
|
||||
supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -95,6 +97,7 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
|
||||
case "biography":
|
||||
context.tab = context.tabs.biography
|
||||
context.xpLogEmpty = (doc.system.xp?.log?.length ?? 0) === 0
|
||||
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
doc.system.description, { async: true })
|
||||
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
|
||||
@@ -111,36 +114,36 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
return context
|
||||
}
|
||||
|
||||
static #onCreateAnomaly() {
|
||||
static async #onCreateAnomaly() {
|
||||
if (this.document.itemTypes.anomaly.length > 0) {
|
||||
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly"))
|
||||
return
|
||||
}
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateAspect() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
static async #onCreateAspect() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateEquipment() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
static async #onCreateEquipment() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("TYPES.Item.equipment"), type: "equipment",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateWeapon() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
static async #onCreateWeapon() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("TYPES.Item.weapon"), type: "weapon",
|
||||
}])
|
||||
}
|
||||
|
||||
static #onCreateArmure() {
|
||||
this.document.createEmbeddedDocuments("Item", [{
|
||||
static async #onCreateArmure() {
|
||||
await this.document.createEmbeddedDocuments("Item", [{
|
||||
name: game.i18n.localize("TYPES.Item.armure"), type: "armure",
|
||||
}])
|
||||
}
|
||||
@@ -163,4 +166,73 @@ export default class CelestopolCharacterSheet extends CelestopolActorSheet {
|
||||
if (!anomaly) return
|
||||
await anomaly.update({ "system.usesRemaining": anomaly.system.level })
|
||||
}
|
||||
|
||||
/** Ouvre un dialogue pour dépenser de l'XP. */
|
||||
static async #onDepenseXp() {
|
||||
const actor = this.document
|
||||
const currentXp = actor.system.xp?.actuel ?? 0
|
||||
const i18n = game.i18n
|
||||
|
||||
const content = `
|
||||
<form class="cel-dialog-form">
|
||||
<div class="form-group">
|
||||
<label>${i18n.localize("CELESTOPOL.XP.montant")}</label>
|
||||
<input type="number" name="montant" value="1" min="1" max="${currentXp}" autofocus />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>${i18n.localize("CELESTOPOL.XP.raison")}</label>
|
||||
<input type="text" name="raison" placeholder="${i18n.localize("CELESTOPOL.XP.raisonPlaceholder")}" />
|
||||
</div>
|
||||
<p class="xp-dialog-hint">${i18n.format("CELESTOPOL.XP.disponible", { n: currentXp })}</p>
|
||||
</form>`
|
||||
|
||||
const result = await foundry.applications.api.DialogV2.prompt({
|
||||
window: { title: i18n.localize("CELESTOPOL.XP.depenser") },
|
||||
content,
|
||||
ok: {
|
||||
label: i18n.localize("CELESTOPOL.XP.confirmer"),
|
||||
callback: (event, button) => {
|
||||
const form = button.form
|
||||
return {
|
||||
montant: parseInt(form.querySelector("[name=montant]").value) || 0,
|
||||
raison: form.querySelector("[name=raison]").value.trim(),
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) return
|
||||
const { montant, raison } = result
|
||||
|
||||
if (montant <= 0) {
|
||||
ui.notifications.warn(i18n.localize("CELESTOPOL.XP.montantInvalide"))
|
||||
return
|
||||
}
|
||||
if (montant > currentXp) {
|
||||
ui.notifications.warn(i18n.format("CELESTOPOL.XP.insuffisant", { n: currentXp }))
|
||||
return
|
||||
}
|
||||
|
||||
const date = new Date().toLocaleDateString("fr-FR")
|
||||
const log = [...(actor.system.xp.log ?? []), { montant, raison, date }]
|
||||
await actor.update({
|
||||
"system.xp.actuel": currentXp - montant,
|
||||
"system.xp.log": log,
|
||||
})
|
||||
}
|
||||
|
||||
/** Supprime une entrée du log XP et rembourse les points (mode édition). */
|
||||
static async #onSupprimerXpLog(event, target) {
|
||||
const idx = parseInt(target.dataset.idx)
|
||||
const actor = this.document
|
||||
const log = [...(actor.system.xp.log ?? [])]
|
||||
if (isNaN(idx) || idx < 0 || idx >= log.length) return
|
||||
|
||||
const entry = log[idx]
|
||||
log.splice(idx, 1)
|
||||
await actor.update({
|
||||
"system.xp.actuel": (actor.system.xp?.actuel ?? 0) + entry.montant,
|
||||
"system.xp.log": log,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user