/** * 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() _setupAnomaliesFolder() } }) /** 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, }) // Suivi de l'import des anomalies (caché) game.settings.register(SYSTEM_ID, "anomaliesImported", { scope: "world", config: false, 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 } } } /* ─── Import initial des anomalies du compendium dans le monde ─────────── */ /** * Au premier lancement (GM uniquement), crée le dossier « Anomalies » dans * les Items du monde et y importe tous les items du compendium anomalies. */ async function _setupAnomaliesFolder() { if (game.settings.get(SYSTEM_ID, "anomaliesImported")) return const pack = game.packs.get(`${SYSTEM_ID}.anomalies`) if (!pack) { console.warn(`${SYSTEM_ID} | Compendium anomalies introuvable`) return } console.log(`${SYSTEM_ID} | Premier lancement : import des anomalies dans le monde`) // Créer le dossier « Anomalies » dans les Items const folder = await Folder.create({ name: "Anomalies", type: "Item", color: "#1b3828", }) // Importer tous les items du compendium dans ce dossier await pack.importAll({ folderId: folder.id, keepId: true }) await game.settings.set(SYSTEM_ID, "anomaliesImported", true) console.log(`${SYSTEM_ID} | Anomalies importées avec succès dans le dossier "${folder.name}"`) ui.notifications.info("Célestopol 1922 | Anomalies importées dans le dossier Items.") } /* ─── 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) })