feat: système de récupération des résistances

- Dialogue de récupération accessible depuis la feuille personnage
- Coupe/Denier : test difficulté 7 (1x/jour), +1 si réussi
- Épée : +1 manuel (repos/soins) ou test guérison rapide
- Bâton : +1 manuel (appréciation MJ)
- Suivi des dates de dernière récupération par symbole
- Désactive le test si déjà fait aujourd'hui
- Bouton d'accès intégré dans la section résistances (icône lit)
- Réutilise HamalronTirageTarot.prompt pour les tests
This commit is contained in:
2026-07-10 20:49:09 +02:00
parent 8a27211ff5
commit 2bd236fb29
5 changed files with 174 additions and 1 deletions
+11
View File
@@ -828,6 +828,17 @@
"Progress": "{name} — {current}/{total} succès", "Progress": "{name} — {current}/{total} succès",
"DeleteTitle": "Supprimer l'action", "DeleteTitle": "Supprimer l'action",
"DeleteWarning": "Voulez-vous vraiment supprimer cette action complexe ? Les données seront perdues." "DeleteWarning": "Voulez-vous vraiment supprimer cette action complexe ? Les données seront perdues."
},
"Recovery": {
"Title": "Récupération des résistances",
"Subtitle": "Récupérer vos points de résistance perdus",
"Today": "Aujourd'hui : {date}",
"Test": "Tester (dif. 7)",
"HealTest": "Test guérison rapide",
"Done": "Déjà fait aujourd'hui",
"Last": "Dernière récup",
"Success": "Récupération réussie ! +1 en {label}",
"Failure": "Échec de la récupération en {label}. Réessayez demain."
} }
} }
} }
+148
View File
@@ -0,0 +1,148 @@
import { SYSTEM } from "../config/system.mjs"
export default class HamalronResistanceRecovery {
static SYMBOLE_LABELS = {
coupe: "Coupe",
epee: "Épée",
denier: "Denier",
baton: "Bâton",
}
static async prompt(actor) {
const resistances = actor.system.resistances
const recovery = actor.system.resistanceRecovery || {}
const today = new Date().toISOString().split("T")[0]
const todayNum = Number(new Date().toISOString().split("T")[0].replace(/-/g, ""))
const symInfos = Object.entries(SYSTEM.TAROT_SYMBOLES).map(([key, val]) => {
const r = resistances[key]
const lastDate = recovery[key]
const lastNum = lastDate ? Number(lastDate.replace(/-/g, "")) : 0
const canRecover = key === "epee"
? true
: (lastNum < todayNum)
const isDenierCoupe = key === "denier" || key === "coupe"
return {
id: key,
label: val.label,
value: r?.value ?? 0,
max: r?.max ?? 0,
canRecover,
isDenierCoupe,
lastRecovery: lastDate || "-",
}
})
const content = HamalronResistanceRecovery.#renderContent(actor, symInfos)
const result = await foundry.applications.api.DialogV2.wait({
window: { title: game.i18n.localize("HAMALRON.Recovery.Title") },
classes: ["fvtt-hamalron"],
position: { width: 480 },
content,
buttons: [
{
action: "close",
label: game.i18n.localize("Cancel"),
default: true,
},
],
actions: {
recoverTest: async (event, button, dialog) => {
const btn = event.currentTarget
const symbole = btn.dataset.symbole
if (!symbole) return
const rollItem = { name: HamalronResistanceRecovery.SYMBOLE_LABELS[symbole] || symbole, value: 0 }
await HamalronResistanceRecovery.#doRecoveryTest(actor, symbole, rollItem)
HamalronResistanceRecovery.prompt(actor)
},
adjustResistance: async (event, button, dialog) => {
const btn = event.currentTarget
const symbole = btn.dataset.symbole
const delta = Number(btn.dataset.delta) || 0
if (!symbole) return
const resistance = actor.system.resistances[symbole]
if (!resistance) return
const newValue = Math.max(0, Math.min(resistance.max, (resistance.value || 0) + delta))
await actor.update({ [`system.resistances.${symbole}.value`]: newValue })
HamalronResistanceRecovery.prompt(actor)
},
},
rejectClose: false,
})
}
static #renderContent(actor, symInfos) {
const today = new Date().toISOString().split("T")[0]
let html = `<div class="fvtt-hamalron-recovery"><h3>${game.i18n.localize("HAMALRON.Recovery.Subtitle")}</h3>`
html += `<p class="recovery-date">${game.i18n.format("HAMALRON.Recovery.Today", { date: today })}</p>`
html += `<div class="recovery-grid">`
for (const info of symInfos) {
const pct = info.max > 0 ? Math.round((info.value / info.max) * 100) : 0
html += `<div class="recovery-card">
<div class="recovery-header">
<strong>${info.label}</strong>
<span>${info.value}/${info.max}</span>
</div>
<div class="progress-bar-mini"><div class="progress-fill" style="width:${pct}%"></div></div>
<div class="recovery-actions">`
if (info.isDenierCoupe) {
if (info.canRecover) {
html += `<button type="button" class="recovery-btn" data-action="recoverTest" data-symbole="${info.id}">
<i class="fas fa-dice"></i> ${game.i18n.localize("HAMALRON.Recovery.Test")}</button>`
} else {
html += `<span class="recovery-done"><i class="fas fa-check-circle"></i> ${game.i18n.localize("HAMALRON.Recovery.Done")}</span>`
}
} else if (info.id === "epee") {
html += `<button type="button" class="recovery-btn-sm" data-action="adjustResistance" data-symbole="epee" data-delta="1">
<i class="fas fa-plus"></i> +1</button>`
html += `<button type="button" class="recovery-btn-sm" data-action="recoverTest" data-symbole="epee">
<i class="fas fa-dice"></i> ${game.i18n.localize("HAMALRON.Recovery.HealTest")}</button>`
} else { // baton
html += `<button type="button" class="recovery-btn-sm" data-action="adjustResistance" data-symbole="baton" data-delta="1">
<i class="fas fa-plus"></i> +1</button>`
}
html += `<button type="button" class="recovery-btn-sm" data-action="adjustResistance" data-symbole="${info.id}" data-delta="-1">
<i class="fas fa-minus"></i></button>`
html += `</div><div class="recovery-last">${game.i18n.localize("HAMALRON.Recovery.Last")}: ${info.lastRecovery}</div>
</div>`
}
html += `</div></div>`
return html
}
static async #doRecoveryTest(actor, symbole, rollItem) {
const niveauData = SYSTEM.NIVEAU_COMPETENCES.inexperimente
const roll = await HamalronTirageTarot.prompt({
rollType: "stat",
rollItem,
actorId: actor.id,
actorName: actor.name,
actorImage: actor.img,
hasTarget: false,
difficulty: "7",
})
if (!roll) return
const today = new Date().toISOString().split("T")[0]
const recoveryData = foundry.utils.duplicate(actor.system.resistanceRecovery || {})
recoveryData[symbole] = today
await actor.update({ [`system.resistanceRecovery`]: recoveryData })
if (roll.total >= 7) {
const resistance = actor.system.resistances[symbole]
if (resistance && resistance.value < resistance.max) {
const newValue = Math.min(resistance.max, (resistance.value || 0) + 1)
await actor.update({ [`system.resistances.${symbole}.value`]: newValue })
}
ui.notifications.info(game.i18n.format("HAMALRON.Recovery.Success", { label: HamalronResistanceRecovery.SYMBOLE_LABELS[symbole] }))
} else {
ui.notifications.info(game.i18n.format("HAMALRON.Recovery.Failure", { label: HamalronResistanceRecovery.SYMBOLE_LABELS[symbole] }))
}
}
}
@@ -18,6 +18,7 @@ export default class HamalronPersonnageSheet extends HamalronActorSheet {
createArmor: HamalronPersonnageSheet.#onCreateArmor, createArmor: HamalronPersonnageSheet.#onCreateArmor,
createWeapon: HamalronPersonnageSheet.#onCreateWeapon, createWeapon: HamalronPersonnageSheet.#onCreateWeapon,
createSortilege: HamalronPersonnageSheet.#onCreateSortilege, createSortilege: HamalronPersonnageSheet.#onCreateSortilege,
openRecovery: HamalronPersonnageSheet.#onOpenRecovery,
modifyResistance: HamalronPersonnageSheet.#onModifyResistance, modifyResistance: HamalronPersonnageSheet.#onModifyResistance,
discardCard: HamalronPersonnageSheet.#onDiscardCard, discardCard: HamalronPersonnageSheet.#onDiscardCard,
rollCompetence: HamalronPersonnageSheet.#onRollCompetence, rollCompetence: HamalronPersonnageSheet.#onRollCompetence,
@@ -173,6 +174,11 @@ export default class HamalronPersonnageSheet extends HamalronActorSheet {
this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HAMALRON.Label.newSortilege"), type: "sortilege" }]) this.document.createEmbeddedDocuments("Item", [{ name: game.i18n.localize("HAMALRON.Label.newSortilege"), type: "sortilege" }])
} }
static async #onOpenRecovery(event, target) {
const { default: HamalronResistanceRecovery } = await import("../resistance-recovery.mjs")
await HamalronResistanceRecovery.prompt(this.document)
}
static async #onModifyResistance(event, target) { static async #onModifyResistance(event, target) {
const symbole = target.dataset.symbole const symbole = target.dataset.symbole
const delta = Number.parseInt(target.dataset.delta) const delta = Number.parseInt(target.dataset.delta)
+1
View File
@@ -35,6 +35,7 @@ export default class HamalronPersonnage extends foundry.abstract.TypeDataModel {
schema.historial = new fields.HTMLField({ required: true, textSearch: true }) schema.historial = new fields.HTMLField({ required: true, textSearch: true })
schema.progression = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 }) schema.progression = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
schema.rangMagie = new fields.StringField({ required: true, initial: "initie", choices: Object.fromEntries(Object.entries(SYSTEM.RANG_MAGICIEN).map(([key, val]) => [key, val.label])) }) schema.rangMagie = new fields.StringField({ required: true, initial: "initie", choices: Object.fromEntries(Object.entries(SYSTEM.RANG_MAGICIEN).map(([key, val]) => [key, val.label])) })
schema.resistanceRecovery = new fields.ObjectField({ required: false, initial: {} })
schema.biodata = new fields.SchemaField({ schema.biodata = new fields.SchemaField({
age: new fields.StringField({ required: true, nullable: false, initial: "" }), age: new fields.StringField({ required: true, nullable: false, initial: "" }),
+8 -1
View File
@@ -138,7 +138,14 @@
</fieldset> </fieldset>
<fieldset class="character-resistances"> <fieldset class="character-resistances">
<legend>{{localize "HAMALRON.Label.resistances"}}</legend> <legend>
{{localize "HAMALRON.Label.resistances"}}
{{#unless isEditMode}}
<a class="control" data-action="openRecovery" data-tooltip="{{localize 'HAMALRON.Recovery.Title'}}" data-tooltip-direction="UP">
<i class="fas fa-bed"></i>
</a>
{{/unless}}
</legend>
<div class="resistances-grid"> <div class="resistances-grid">
{{#each system.resistances as |resistance symbole|}} {{#each system.resistances as |resistance symbole|}}
<div class="resistance-stat"> <div class="resistance-stat">