/** * 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 2025–2026 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 = `
` 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) } }