Some checks failed
Release Creation / build (release) Failing after 1m24s
317 lines
13 KiB
JavaScript
317 lines
13 KiB
JavaScript
/**
|
||
* 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 = `
|
||
<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)
|
||
}
|
||
}
|