Files
fvtt-celestopol/module/applications/sheets/character-sheet.mjs
LeRatierBretonnier 44cc07db73
Some checks failed
Release Creation / build (release) Failing after 1m24s
Portraits et corrections sur valeurs des PNJ
2026-04-12 11:52:17 +02:00

317 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Célestopol 1922 — Système FoundryVTT
*
* Célestopol 1922 est un jeu de rôle édité par Antre-Monde Éditions.
* Ce système FoundryVTT est une implémentation indépendante et n'est pas
* affilié à Antre-Monde Éditions,
* mais a été réalisé avec l'autorisation d'Antre-Monde Éditions.
*
* @author LeRatierBretonnien
* @copyright 20252026 LeRatierBretonnien
* @license CC BY-NC-SA 4.0 https://creativecommons.org/licenses/by-nc-sa/4.0/
*/
import CelestopolActorSheet from "./base-actor-sheet.mjs"
import { SYSTEM } from "../../config/system.mjs"
export default class CelestopolCharacterSheet extends CelestopolActorSheet {
/** @override */
static DEFAULT_OPTIONS = {
classes: ["character"],
position: { width: 920, height: 660 },
window: { contentClasses: ["character-content"] },
actions: {
createAnomaly: CelestopolCharacterSheet.#onCreateAnomaly,
createAspect: CelestopolCharacterSheet.#onCreateAspect,
createEquipment: CelestopolCharacterSheet.#onCreateEquipment,
createWeapon: CelestopolCharacterSheet.#onCreateWeapon,
createArmure: CelestopolCharacterSheet.#onCreateArmure,
useAnomaly: CelestopolCharacterSheet.#onUseAnomaly,
resetAnomalyUses: CelestopolCharacterSheet.#onResetAnomalyUses,
depenseXp: CelestopolCharacterSheet.#onDepenseXp,
supprimerXpLog: CelestopolCharacterSheet.#onSupprimerXpLog,
rollMoonDie: CelestopolCharacterSheet.#onRollMoonDie,
manageFactionAspects: CelestopolCharacterSheet.#onManageFactionAspects,
},
}
/** @override */
static PARTS = {
main: { template: "systems/fvtt-celestopol/templates/character-main.hbs" },
tabs: { template: "templates/generic/tab-navigation.hbs" },
competences:{ template: "systems/fvtt-celestopol/templates/character-competences.hbs" },
blessures: { template: "systems/fvtt-celestopol/templates/character-blessures.hbs" },
factions: { template: "systems/fvtt-celestopol/templates/character-factions.hbs" },
equipement: { template: "systems/fvtt-celestopol/templates/character-equipement.hbs" },
biography: { template: "systems/fvtt-celestopol/templates/character-biography.hbs" },
}
tabGroups = { sheet: "competences" }
#getTabs() {
const tabs = {
competences:{ id: "competences", group: "sheet", icon: "fa-solid fa-dice-d6", label: "CELESTOPOL.Tab.competences" },
blessures: { id: "blessures", group: "sheet", icon: "fa-solid fa-heart-crack", label: "CELESTOPOL.Tab.blessures" },
factions: { id: "factions", group: "sheet", icon: "fa-solid fa-flag", label: "CELESTOPOL.Tab.factions" },
equipement: { id: "equipement", group: "sheet", icon: "fa-solid fa-shield-halved",label: "CELESTOPOL.Tab.equipement" },
biography: { id: "biography", group: "sheet", icon: "fa-solid fa-book", label: "CELESTOPOL.Tab.biography" },
}
for (const v of Object.values(tabs)) {
v.active = this.tabGroups[v.group] === v.id
v.cssClass = v.active ? "active" : ""
}
return tabs
}
/** @override */
async _prepareContext() {
const context = await super._prepareContext()
context.tabs = this.#getTabs()
context.stats = SYSTEM.STATS
context.skills = SYSTEM.SKILLS
context.anomalyTypes = SYSTEM.ANOMALY_TYPES
context.factions = SYSTEM.FACTIONS
context.woundLevels = SYSTEM.WOUND_LEVELS
context.selectedPrimaryFactionId = game.celestopol?.normalizeFactionId(this.document.system.faction) || ""
context.legacyPrimaryFactionValue = this.document.system.faction && !context.selectedPrimaryFactionId
? `${this.document.system.faction}`.trim()
: ""
context.primaryFactionLabel = game.celestopol?.getFactionDisplayLabel(this.document.system.faction) || this.document.system.faction
return context
}
/** @override */
async _preparePartContext(partId, context) {
context.systemFields = this.document.system.schema.fields
const doc = this.document
switch (partId) {
case "main":
break
case "competences":
context.tab = context.tabs.competences
context.anomaly = doc.itemTypes.anomaly[0] ?? null
context.aspects = doc.itemTypes.aspect
if (context.anomaly) {
const def = SYSTEM.ANOMALY_DEFINITIONS[context.anomaly.system.subtype] ?? SYSTEM.ANOMALY_DEFINITIONS.none
context.anomalySkillLabels = def.technicalSkills.map(key => {
if (key === "lune") return game.i18n.localize("CELESTOPOL.Anomaly.moonDie")
for (const skills of Object.values(SYSTEM.SKILLS)) {
if (skills[key]) return game.i18n.localize(skills[key].label)
}
return key
})
} else {
context.anomalySkillLabels = []
}
break
case "blessures":
context.tab = context.tabs.blessures
break
case "factions":
context.tab = context.tabs.factions
context.factionAspectSummary = game.celestopol?.getFactionAspectSummary(this.document) ?? null
context.factionLegend = [
{ value: "+4", label: game.i18n.localize("CELESTOPOL.Faction.levelAllies") },
{ value: "+3", label: game.i18n.localize("CELESTOPOL.Faction.levelAmicaux") },
{ value: "+2", label: game.i18n.localize("CELESTOPOL.Faction.levelPartenaires") },
{ value: "+1", label: game.i18n.localize("CELESTOPOL.Faction.levelBienveillants") },
{ value: "0", label: game.i18n.localize("CELESTOPOL.Faction.levelNeutres") },
{ value: "-1", label: game.i18n.localize("CELESTOPOL.Faction.levelMefiants") },
{ value: "-2", label: game.i18n.localize("CELESTOPOL.Faction.levelHostiles") },
{ value: "-3", label: game.i18n.localize("CELESTOPOL.Faction.levelRivaux") },
{ value: "-4", label: game.i18n.localize("CELESTOPOL.Faction.levelEnnemis") },
]
context.factionRows = Object.entries(SYSTEM.FACTIONS).map(([id, fDef]) => {
const val = this.document.system.factions[id]?.value ?? 0
return {
id,
label: fDef.label,
value: val,
valueStr: val > 0 ? `+${val}` : `${val}`,
dots: Array.from({ length: 9 }, (_, i) => ({
index: i,
filled: i <= val + 4,
type: i < 4 ? "neg" : i === 4 ? "neutral" : "pos",
})),
}
})
context.factionCustom = ["perso1", "perso2"].map(id => {
const f = this.document.system.factions[id]
const val = f?.value ?? 0
return {
id,
label: f?.label ?? "",
value: val,
valueStr: val > 0 ? `+${val}` : `${val}`,
dots: Array.from({ length: 9 }, (_, i) => ({
index: i,
filled: i <= val + 4,
type: i < 4 ? "neg" : i === 4 ? "neutral" : "pos",
})),
}
})
break
case "biography":
context.tab = context.tabs.biography
context.xpLogEmpty = (doc.system.xp?.log?.length ?? 0) === 0
context.biographyPortrait = doc.system.portraitImage || ""
context.hasBiographyPortrait = !!doc.system.portraitImage
context.enrichedDescriptionPhysique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.descriptionPhysique, { relativeTo: this.document })
context.enrichedDescriptionPsychologique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.descriptionPsychologique, { relativeTo: this.document })
context.enrichedHistorique = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.historique, { relativeTo: this.document })
context.enrichedNotes = await foundry.applications.ux.TextEditor.implementation.enrichHTML(
doc.system.notes, { relativeTo: this.document })
break
case "equipement":
context.tab = context.tabs.equipement
context.weapons = doc.itemTypes.weapon.sort((a, b) => a.name.localeCompare(b.name))
context.armures = doc.itemTypes.armure.sort((a, b) => a.name.localeCompare(b.name))
context.equipments= doc.itemTypes.equipment.sort((a, b) => a.name.localeCompare(b.name))
break
}
return context
}
static async #onCreateAnomaly() {
if (this.document.itemTypes.anomaly.length > 0) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.maxAnomaly"))
return
}
await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("CELESTOPOL.Item.newAnomaly"), type: "anomaly",
}])
}
static async #onCreateAspect() {
await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("CELESTOPOL.Item.newAspect"), type: "aspect",
}])
}
static async #onCreateEquipment() {
await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.equipment"), type: "equipment",
}])
}
static async #onCreateWeapon() {
await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.weapon"), type: "weapon",
}])
}
static async #onCreateArmure() {
await this.document.createEmbeddedDocuments("Item", [{
name: game.i18n.localize("TYPES.Item.armure"), type: "armure",
system: { protection: 1, malus: 1 },
}])
}
static async #onUseAnomaly(event, target) {
const itemId = target.dataset.itemId
const anomaly = this.document.items.get(itemId)
if (!anomaly) return
const current = anomaly.system.usesRemaining
if (current <= 0) {
ui.notifications.warn(game.i18n.localize("CELESTOPOL.Anomaly.noUsesLeft"))
return
}
await anomaly.update({ "system.usesRemaining": current - 1 })
}
static async #onResetAnomalyUses(event, target) {
const itemId = target.dataset.itemId
const anomaly = this.document.items.get(itemId)
if (!anomaly) return
await anomaly.update({ "system.usesRemaining": anomaly.system.level })
}
static async #onManageFactionAspects() {
await game.celestopol?.manageFactionAspects(this.document)
}
/** 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,
})
}
/** Lance le Dé de la Lune de façon autonome depuis le header de la fiche. */
static async #onRollMoonDie() {
const { CelestopolRoll } = await import("../../documents/roll.mjs")
await CelestopolRoll.rollMoonStandalone(this.document)
}
}