Files
fvtt-celestopol/fvtt-celestopol.mjs
LeRatierBretonnier b5c40971c4 fix: logo de pause — correction sélecteur CSS et hook v13
- Patch GamePause.prototype._prepareContext dans le hook init
  (hook renderPause n'existe pas en v13, classe = GamePause)
- CSS : #pause img (non #pause figure img car #pause est lui-même le <figure>)
- Taille 600px / 50vw, halo doré, figcaption masquée

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 01:16:25 +02:00

365 lines
14 KiB
JavaScript

/**
* fvtt-celestopol.mjs — Point d'entrée principal du système Célestopol 1922
* FoundryVTT v13+ / DataModels / ApplicationV2
*/
import { SYSTEM, SYSTEM_ID, ASCII } from "./module/config/system.mjs"
import {
CelestopolCharacter,
CelestopolNPC,
CelestopolAnomaly,
CelestopolAspect,
CelestopolEquipment,
CelestopolWeapon,
CelestopolArmure,
} from "./module/models/_module.mjs"
import {
CelestopolActor,
CelestopolItem,
CelestopolChatMessage,
CelestopolRoll,
} from "./module/documents/_module.mjs"
import {
CelestopolCharacterSheet,
CelestopolNPCSheet,
CelestopolAnomalySheet,
CelestopolAspectSheet,
CelestopolEquipmentSheet,
CelestopolWeaponSheet,
CelestopolArmureSheet,
} from "./module/applications/_module.mjs"
/* ─── Init hook ──────────────────────────────────────────────────────────── */
Hooks.once("init", () => {
console.log(ASCII)
console.log(`${SYSTEM_ID} | Initializing Célestopol 1922 system`)
// Logo de pause : patch de GamePause._prepareContext pour remplacer l'icône
const GamePause = foundry.applications.hud?.GamePause ?? globalThis.GamePause
if (GamePause) {
const _origCtx = GamePause.prototype._prepareContext
GamePause.prototype._prepareContext = async function(options) {
const ctx = await _origCtx.call(this, options)
ctx.icon = "systems/fvtt-celestopol/assets/ui/logo_jeu.png"
ctx.spin = false
return ctx
}
}
// Expose SYSTEM constants in game.system namespace
game.celestopol = { SYSTEM }
// ── DataModels ──────────────────────────────────────────────────────────
CONFIG.Actor.dataModels.character = CelestopolCharacter
CONFIG.Actor.dataModels.npc = CelestopolNPC
CONFIG.Item.dataModels.anomaly = CelestopolAnomaly
CONFIG.Item.dataModels.aspect = CelestopolAspect
CONFIG.Item.dataModels.equipment = CelestopolEquipment
CONFIG.Item.dataModels.weapon = CelestopolWeapon
CONFIG.Item.dataModels.armure = CelestopolArmure
// ── Document classes ────────────────────────────────────────────────────
CONFIG.Actor.documentClass = CelestopolActor
CONFIG.Item.documentClass = CelestopolItem
CONFIG.ChatMessage.documentClass = CelestopolChatMessage
CONFIG.Dice.rolls.push(CelestopolRoll)
// ── Token display defaults ───────────────────────────────────────────────
CONFIG.Actor.trackableAttributes = {
character: {
bar: ["blessures.lvl"],
value: ["initiative", "anomaly.level"],
},
npc: {
bar: ["blessures.lvl"],
value: ["initiative"],
},
}
// ── Sheets: unregister core, register system sheets ─────────────────────
foundry.applications.sheets.ActorSheetV2.unregisterSheet?.("core", "Actor", { types: ["character", "npc"] })
foundry.appv1?.sheets?.ActorSheet && foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet)
foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, CelestopolCharacterSheet, {
types: ["character"],
makeDefault: true,
label: "CELESTOPOL.Sheet.character",
})
foundry.documents.collections.Actors.registerSheet(SYSTEM_ID, CelestopolNPCSheet, {
types: ["npc"],
makeDefault: true,
label: "CELESTOPOL.Sheet.npc",
})
foundry.appv1?.sheets?.ItemSheet && foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet)
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolAnomalySheet, {
types: ["anomaly"],
makeDefault: true,
label: "CELESTOPOL.Sheet.anomaly",
})
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolAspectSheet, {
types: ["aspect"],
makeDefault: true,
label: "CELESTOPOL.Sheet.aspect",
})
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolEquipmentSheet, {
types: ["equipment"],
makeDefault: true,
label: "CELESTOPOL.Sheet.equipment",
})
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolWeaponSheet, {
types: ["weapon"],
makeDefault: true,
label: "CELESTOPOL.Sheet.weapon",
})
foundry.documents.collections.Items.registerSheet(SYSTEM_ID, CelestopolArmureSheet, {
types: ["armure"],
makeDefault: true,
label: "CELESTOPOL.Sheet.armure",
})
// ── Handlebars helpers ───────────────────────────────────────────────────
_registerHandlebarsHelpers()
// ── Game settings ────────────────────────────────────────────────────────
_registerSettings()
// ── Pre-load templates ───────────────────────────────────────────────────
_preloadTemplates()
})
/* ─── Ready hook ─────────────────────────────────────────────────────────── */
/* ─── Ready hook ─────────────────────────────────────────────────────────── */
Hooks.once("ready", () => {
console.log(`${SYSTEM_ID} | System ready`)
// Socket handler for GM-only operations (e.g. wound application)
if (game.socket) {
game.socket.on(`system.${SYSTEM_ID}`, _onSocketMessage)
}
// Migration : supprime les items de types obsolètes (ex: "attribute")
if (game.user.isGM) {
_migrateObsoleteItems()
_migrateIntegerTracks()
}
})
/** Supprime les items dont le type n'est plus reconnu par le système. */
async function _migrateObsoleteItems() {
const validTypes = new Set(["anomaly", "aspect", "equipment", "weapon", "armure"])
for (const actor of game.actors) {
// Utilise _source.items pour trouver les items qui n'ont pas pu s'initialiser
const toDelete = (actor._source?.items ?? [])
.filter(i => !validTypes.has(i.type))
.map(i => i._id)
if (toDelete.length) {
console.warn(`${SYSTEM_ID} | Migration: suppression de ${toDelete.length} item(s) obsolète(s) sur ${actor.name}`, toDelete)
await actor.deleteEmbeddedDocuments("Item", toDelete)
}
}
// Items globaux (hors acteur)
const globalToDelete = game.items.contents
.filter(i => !validTypes.has(i.type))
.map(i => i.id)
if (globalToDelete.length) {
console.warn(`${SYSTEM_ID} | Migration: suppression de ${globalToDelete.length} item(s) global(aux) obsolète(s)`, globalToDelete)
await Item.deleteDocuments(globalToDelete)
}
}
/**
* Migration : convertit les anciennes données booléennes (level1..level8, b1..b8, etc.)
* vers le nouveau stockage entier direct.
* Ne s'applique qu'aux acteurs ayant encore l'ancien format dans leur source.
*/
async function _migrateIntegerTracks() {
const validActors = game.actors.contents.filter(a => ["character", "npc"].includes(a.type))
for (const actor of validActors) {
const src = actor._source?.system
if (!src) continue
const updateData = {}
// Blessures : si b1 existe dans la source, recalculer lvl depuis les booléens
const blessures = src.blessures ?? {}
if ("b1" in blessures) {
const lvl = [1,2,3,4,5,6,7,8].filter(i => blessures[`b${i}`]?.checked === true).length
updateData["system.blessures.lvl"] = lvl
}
if (actor.type === "character") {
// Destin
const destin = src.destin ?? {}
if ("d1" in destin) {
const lvl = [1,2,3,4,5,6,7,8].filter(i => destin[`d${i}`]?.checked === true).length
updateData["system.destin.lvl"] = lvl
}
// Spleen
const spleen = src.spleen ?? {}
if ("s1" in spleen) {
const lvl = [1,2,3,4,5,6,7,8].filter(i => spleen[`s${i}`]?.checked === true).length
updateData["system.spleen.lvl"] = lvl
}
// Domaines : si level1 existe dans un domaine, recalculer value depuis les booléens
const stats = src.stats ?? {}
for (const [statId, statData] of Object.entries(stats)) {
for (const [skillId, skill] of Object.entries(statData ?? {})) {
if (typeof skill !== "object" || !("level1" in skill)) continue
const value = [1,2,3,4,5,6,7,8].filter(i => skill[`level${i}`] === true).length
updateData[`system.stats.${statId}.${skillId}.value`] = value
}
}
// Factions : si level1 existe dans une faction, recalculer value depuis les booléens
const factions = src.factions ?? {}
for (const [factionId, faction] of Object.entries(factions)) {
if (typeof faction !== "object" || !("level1" in faction)) continue
const value = [1,2,3,4,5,6,7,8,9].filter(i => faction[`level${i}`] === true).length
updateData[`system.factions.${factionId}.value`] = value
}
}
if (Object.keys(updateData).length > 0) {
console.log(`${SYSTEM_ID} | Migration tracks → entiers : ${actor.name}`, updateData)
await actor.update(updateData)
}
}
}
/* ─── Handlebars helpers ─────────────────────────────────────────────────── */
function _registerHandlebarsHelpers() {
// Helper : concat strings
Handlebars.registerHelper("concat", (...args) => args.slice(0, -1).join(""))
// Helper : strict equality
Handlebars.registerHelper("eq", (a, b) => a === b)
// Helper : greater than
Handlebars.registerHelper("gt", (a, b) => a > b)
// Helper : less than or equal
Handlebars.registerHelper("lte", (a, b) => a <= b)
// Helper : greater than or equal
Handlebars.registerHelper("gte", (a, b) => a >= b)
// Helper : less than
Handlebars.registerHelper("lt", (a, b) => a < b)
// Helper : logical OR
Handlebars.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean))
// Helper : build array from args (Handlebars doesn't have arrays natively)
Handlebars.registerHelper("array", (...args) => args.slice(0, -1))
// Helper : range(n) → [1, 2, ..., n] — pour les boucles de cases à cocher
Handlebars.registerHelper("range", (n) => Array.from({ length: n }, (_, i) => i + 1))
// Helper : nested object lookup with dot path or multiple keys
Handlebars.registerHelper("lookup", (obj, ...args) => {
const options = args.pop() // last arg is Handlebars options hash
return args.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj)
})
// Helper : negate a number (abs value helper)
Handlebars.registerHelper("neg", n => -n)
// Helper : absolute value
Handlebars.registerHelper("abs", n => Math.abs(n))
// Helper : add two numbers
Handlebars.registerHelper("add", (a, b) => a + b)
Handlebars.registerHelper("let", function(value, options) {
return options.fn({ value })
})
}
/* ─── Settings ───────────────────────────────────────────────────────────── */
function _registerSettings() {
game.settings.register(SYSTEM_ID, "rollMoonDieByDefault", {
name: "CELESTOPOL.Setting.rollMoonDieByDefault.name",
hint: "CELESTOPOL.Setting.rollMoonDieByDefault.hint",
scope: "world",
config: true,
type: Boolean,
default: false,
})
}
/* ─── Template preload ───────────────────────────────────────────────────── */
function _preloadTemplates() {
const base = `systems/${SYSTEM_ID}/templates`
foundry.applications.handlebars.loadTemplates([
`${base}/character-main.hbs`,
`${base}/character-competences.hbs`,
`${base}/character-blessures.hbs`,
`${base}/character-factions.hbs`,
`${base}/character-equipement.hbs`,
`${base}/character-biography.hbs`,
`${base}/npc-main.hbs`,
`${base}/npc-competences.hbs`,
`${base}/npc-blessures.hbs`,
`${base}/anomaly.hbs`,
`${base}/aspect.hbs`,
`${base}/equipment.hbs`,
`${base}/weapon.hbs`,
`${base}/armure.hbs`,
`${base}/roll-dialog.hbs`,
`${base}/chat-message.hbs`,
`${base}/partials/item-scores.hbs`,
])
}
/* ─── Socket handler ─────────────────────────────────────────────────────── */
function _onSocketMessage(data) {
if (!game.user.isGM) return
switch (data.type) {
case "applyWound": {
const actor = game.actors.get(data.actorId)
if (actor) actor.update({ "system.blessures.lvl": data.level })
break
}
}
}
/* ─── Nom par défaut des items à la création ─────────────────────────────── */
Hooks.on("preCreateItem", (item, data) => {
const defaultNames = {
weapon: () => game.i18n.localize("TYPES.Item.weapon"),
armure: () => game.i18n.localize("TYPES.Item.armure"),
anomaly: () => game.i18n.localize("TYPES.Item.anomaly"),
aspect: () => game.i18n.localize("TYPES.Item.aspect"),
equipment: () => game.i18n.localize("TYPES.Item.equipment"),
}
const defaultIcons = {
weapon: "systems/fvtt-celestopol/assets/icons/weapon.svg",
armure: "systems/fvtt-celestopol/assets/icons/armure.svg",
anomaly: "systems/fvtt-celestopol/assets/icons/anomaly.svg",
aspect: "systems/fvtt-celestopol/assets/icons/aspect.svg",
equipment: "systems/fvtt-celestopol/assets/icons/equipment.svg",
}
const updates = {}
const fn = defaultNames[item.type]
if (fn && (!data.name || data.name === "New Item" || data.name === item.type)) {
updates.name = fn()
}
const defaultIcon = defaultIcons[item.type]
if (defaultIcon && (!data.img || data.img === "icons/svg/item-bag.svg")) {
updates.img = defaultIcon
}
if (Object.keys(updates).length) item.updateSource(updates)
})