/** * Cthulhu Eternal RPG System * Author: LeRatierBretonnien/Uberwald */ import { SYSTEM } from "./module/config/system.mjs" globalThis.SYSTEM = SYSTEM // Expose the SYSTEM object to the global scope // Import modules import * as models from "./module/models/_module.mjs" import * as documents from "./module/documents/_module.mjs" import * as applications from "./module/applications/_module.mjs" import { handleSocketEvent } from "./module/socket.mjs" import HamalronUtils from "./module/utils.mjs" export class ClassCounter { static printHello() { console.log("Hello") } static sendJsonPostRequest(e, s) { const t = { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(s) }; return fetch(e, t).then((e => { if (!e.ok) throw new Error("La requête a échoué avec le statut " + e.status); return e.json() })).catch((e => { throw console.error("Erreur envoi de la requête:", e), e })) } static registerUsageCount(e = game.system.id, s = {}) { if (game.user.isGM) { game.settings.register(e, "world-key", { name: "Unique world key", scope: "world", config: !1, default: "", type: String }); let t = game.settings.get(e, "world-key"); null != t && "" != t && "NONE" != t && "none" != t.toLowerCase() || (t = foundry.utils.randomID(32), game.settings.set(e, "world-key", t)); let a = { name: e, system: game.system.id, worldKey: t, version: game.system.version, language: game.settings.get("core", "language"), remoteAddr: game.data.addresses.remote, nbInstalledModules: game.modules.size, nbActiveModules: game.modules.filter((e => e.active)).length, nbPacks: game.world.packs.size, nbUsers: game.users.size, nbScenes: game.scenes.size, nbActors: game.actors.size, nbPlaylist: game.playlists.size, nbTables: game.tables.size, nbCards: game.cards.size, optionsData: s, foundryVersion: `${game.release.generation}.${game.release.build}` }; this.sendJsonPostRequest("https://www.uberwald.me/fvtt_appcount/count_post.php", a) } } } Hooks.once("init", function () { console.info("HAMALRON JDR | Initializing System") console.info(SYSTEM.ASCII) globalThis.Hamalron = game.system game.system.CONST = SYSTEM // Expose the system API game.system.api = { applications, models, documents, openTarotDeckManager: () => applications.HamalronTarotDeckManager.openManager(), openActionComplexe: () => applications.HamalronActionComplexe.open(), } // Global reference needed by initiative override globalThis.HamalronTarotDeckManager = applications.HamalronTarotDeckManager CONFIG.Combat.initiative = { formula: "1d6", decimals: 0, } // Override initiative: draw a card from the tarot deck instead of rolling dice Combatant.prototype.getInitiativeRoll = function (formula) { const data = this.actor?.getRollData() ?? {} const roll = new Roll("1d6", data) const combatantName = this.name const origEvaluate = roll.evaluate.bind(roll) roll.evaluate = async function (options = {}) { let deckManager = globalThis.HamalronTarotDeckManager?._instance if (!deckManager) { deckManager = new (globalThis.HamalronTarotDeckManager)() globalThis.HamalronTarotDeckManager._instance = deckManager } if (deckManager.deck.length === 0) { const tarotItems = game.items.filter(i => i.type === "tarot") if (tarotItems.length > 0) deckManager.resetDeck() } if (deckManager.deck.length > 0) { const drawn = deckManager.drawCards(1) if (drawn.length > 0) { const card = drawn[0] const val = SYSTEM.TAROT_VALEURS[card.valeur]?.value ?? 0 this._total = val this._evaluated = true this._formula = "1d1" this._dice = [] this._terms = [] this.options.cardDraw = { name: card.name, img: card.img, valeur: card.valeur, value: val, } if (combatantName) globalThis._initiativeCardMap.set(combatantName, this.options.cardDraw) deckManager.addToDiscard(card) await deckManager.saveDeckState() return this } } return origEvaluate(options) } return roll } CONFIG.Actor.documentClass = documents.HamalronActor CONFIG.Actor.dataModels = { personnage: models.HamalronPersonnage, pnj: models.HamalronPNJ, } CONFIG.Item.documentClass = documents.HamalronItem CONFIG.Item.dataModels = { arme: models.HamalronArme, armure: models.HamalronArmure, competence: models.HamalronCompetence, equipement: models.HamalronEquipement, faction: models.HamalronFaction, langue: models.HamalronLangue, peuple: models.HamalronPeuple, region: models.HamalronRegion, sortilege: models.HamalronSortilege, tarot: models.HamalronTarot, } // Register sheet application classes foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet) foundry.documents.collections.Actors.registerSheet("fvtt-hamalron", applications.HamalronPersonnageSheet, { types: ["personnage"], makeDefault: true }) foundry.documents.collections.Actors.registerSheet("fvtt-hamalron", applications.HamalronEnemySheet, { types: ["pnj"], makeDefault: true }) foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ItemSheet) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronArmeSheet, { types: ["arme"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronArmureSheet, { types: ["armure"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronCompetenceSheet, { types: ["competence"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronEquipementSheet, { types: ["equipement"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronFactionSheet, { types: ["faction"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronLangueSheet, { types: ["langue"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronPeupleSheet, { types: ["peuple"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronRegionSheet, { types: ["region"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronSortilegeSheet, { types: ["sortilege"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("fvtt-hamalron", applications.HamalronTarotSheet, { types: ["tarot"], makeDefault: true }) // Other Document Configuration CONFIG.ChatMessage.documentClass = documents.HamalronChatMessage // Dice system configuration CONFIG.Dice.rolls.push(documents.HamalronRoll) game.settings.register("fvtt-hamalron", "worldKey", { name: "Unique world key", scope: "world", config: false, type: String, default: "", }) game.settings.register("fvtt-hamalron", "tarotDeckState", { name: "Tarot Deck State", hint: "Stores the current state of the tarot deck and discard pile", scope: "world", config: false, type: Object, default: { deck: [], discard: [] }, }) game.settings.register("fvtt-hamalron", "actionComplexeState", { name: "Action Complexe State", hint: "Stores ongoing complex actions", scope: "world", config: false, type: Object, default: { actions: [] }, }) // Activate socket handler game.socket.on(`system.${SYSTEM.id}`, handleSocketEvent) HamalronUtils.registerSettings() HamalronUtils.registerHandlebarsHelpers() console.info("HAMALRON JDR | System Initialized") }) /** * Perform one-time configuration of system configuration objects. */ function preLocalizeConfig() { const localizeConfigObject = (obj, keys) => { for (let o of Object.values(obj)) { for (let k of keys) { o[k] = game.i18n.localize(o[k]) } } } } Hooks.once("ready", function () { console.info("HAMALRON JDR | Ready") if (game.user.isGM) { ClassCounter.registerUsageCount("fvtt-hamalron", {}) } preLocalizeConfig() }) // Add button to scene controls for Tarot Deck Manager and Actions Complexes Hooks.on("getSceneControlButtons", (controls) => { if (game.user.isGM) { const controlsArray = Array.isArray(controls) ? controls : controls.controls || []; const tokenControls = controlsArray.find(c => c.name === "token") if (tokenControls) { tokenControls.tools.push({ name: "tarot-deck", title: "HAMALRON.TarotDeck.Title", icon: "fas fa-cards", button: true, onClick: () => applications.HamalronTarotDeckManager.openManager(), }) tokenControls.tools.push({ name: "action-complexe", title: "HAMALRON.ActionComplexe.Title", icon: "fas fa-tasks", button: true, onClick: () => applications.HamalronActionComplexe.open(), }) } } }) // Add system buttons to the chat sidebar function addChatTools() { const controls = document.querySelector("#chat-controls") if (!controls || controls.querySelector(".hamalron-chat-tools")) return const tools = document.createElement("div") tools.className = "hamalron-chat-tools" tools.style.cssText = "display:flex;gap:4px;padding:4px 8px;border-bottom:1px solid #444;margin-bottom:2px;flex-wrap:wrap" const addBtn = (icon, label, title, onClick) => { const btn = document.createElement("button") btn.type = "button" btn.style.cssText = "display:flex;align-items:center;gap:4px;padding:3px 8px;background:#2a2a4a;color:#e0e0e0;border:1px solid #555;border-radius:4px;cursor:pointer;font-size:11px;white-space:nowrap" btn.innerHTML = ` ${label}` btn.title = title btn.addEventListener("click", onClick) btn.addEventListener("mouseenter", () => { btn.style.background = "#3a3a6a" }) btn.addEventListener("mouseleave", () => { btn.style.background = "#2a2a4a" }) tools.appendChild(btn) } if (game.user?.isGM) { addBtn("fas fa-cards", "Tarot", game.i18n.localize("HAMALRON.TarotDeck.Title"), () => { game.system.api.openTarotDeckManager() }) addBtn("fas fa-tasks", "Actions", game.i18n.localize("HAMALRON.ActionComplexe.Title"), () => { game.system.api.openActionComplexe() }) } addBtn("fas fa-cards", "Épreuve", "Lancer une épreuve (choisir une carte)", async () => { const actor = game.user?.character || game.actors?.find(a => a.type === "personnage" && a.testUserPermission(game.user, "OWNER")) if (!actor) { ui.notifications.warn("Aucun personnage trouvé"); return } const { default: HamalronTirageTarot } = await import("./module/documents/tirage-tarot.mjs") await HamalronTirageTarot.promptEpreuve(actor) }) controls.parentNode.insertBefore(tools, controls) } Hooks.on("renderChatLog", () => setTimeout(addChatTools, 0)) Hooks.on("collapseSidebar", () => setTimeout(addChatTools, 100)) // Store card info from initiative rolls for the chat hook globalThis._initiativeCardMap = new Map() Hooks.on("createChatMessage", async (message, options, userId) => { const speakerName = message.speaker?.alias if (!speakerName) return const card = globalThis._initiativeCardMap.get(speakerName) if (!card) return globalThis._initiativeCardMap.delete(speakerName) await new Promise(r => setTimeout(r, 50)) const actor = game.actors.getName(speakerName) const actorImg = actor?.img || "icons/svg/mystery-man.svg" const total = message.rolls?.[0]?.total ?? "" const content = `
${speakerName}
${total}
${card.name}
${card.name} ${game.i18n.localize("HAMALRON.Initiative.Value")} : ${card.value}
` await message.update({ content }) }) // Helper: ensure deck manager is initialized and has cards async function ensureDeck() { let dm = globalThis.HamalronTarotDeckManager?._instance if (!dm) { const DM = game.system.api.applications.HamalronTarotDeckManager dm = new DM() DM._instance = dm await dm.loadDeckState() } if (dm.deck.length === 0) { await dm.resetDeck() } if (dm.deck.length === 0) { ui.notifications.warn(game.i18n.localize("HAMALRON.Notifications.deckEmpty")) return null } return dm } // Chat action buttons (Sous tension / Opposé) $(document).on("click", "[data-action='sousTension']", async function () { const msgEl = $(this).closest(".chat-message") const msgId = msgEl?.data("messageId") const msg = game.messages?.get(msgId) if (!msg) return const totalText = msgEl.find(".calc-total")?.text()?.trim() const totalJoueur = Number(totalText) || 0 const actorName = msg.speaker?.alias || "Inconnu" const deckManager = await ensureDeck() if (!deckManager) return const drawn = deckManager.drawCards(1) if (!drawn.length) return const card = drawn[0] const valData = SYSTEM.TAROT_VALEURS[card.valeur] const valeurMJ = valData?.value ?? 0 deckManager.addToDiscard(card) await deckManager.saveDeckState() const successA = totalJoueur >= valeurMJ const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-hamalron/templates/chat-tension.hbs", { cssClass: "fvtt-hamalron tension-roll", icon: "fa-bolt", label: game.i18n.localize("HAMALRON.Label.sousTension"), joueurName: actorName, joueurScore: totalJoueur, mjCard: `${card.name} (${valeurMJ})`, mjScore: valeurMJ, isSuccess: successA, isOpponentWinning: totalJoueur < valeurMJ, isSousTension: true }) ChatMessage.create({ content, style: CONST.CHAT_MESSAGE_STYLES.OTHER }) }) $(document).on("click", "[data-action='oppose']", async function () { const msgEl = $(this).closest(".chat-message") const msgId = msgEl?.data("messageId") const msg = game.messages?.get(msgId) if (!msg) return const totalText = msgEl.find(".calc-total")?.text()?.trim() const totalJoueur = Number(totalText) || 0 const actorName = msg.speaker?.alias || "Inconnu" const deckManager = await ensureDeck() if (!deckManager) return const drawn = deckManager.drawCards(1) if (!drawn.length) return const card = drawn[0] const valData = SYSTEM.TAROT_VALEURS[card.valeur] const valeurOpposant = valData?.value ?? 0 deckManager.addToDiscard(card) await deckManager.saveDeckState() const successB = totalJoueur >= valeurOpposant const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-hamalron/templates/chat-tension.hbs", { cssClass: "fvtt-hamalron tension-roll", icon: "fa-shield-halved", label: game.i18n.localize("HAMALRON.Label.oppose"), joueurName: actorName, joueurScore: totalJoueur, mjCard: `${card.name} (${valeurOpposant})`, mjScore: valeurOpposant, isSuccess: successB, isOpponentWinning: totalJoueur < valeurOpposant, isSousTension: false }) ChatMessage.create({ content, style: CONST.CHAT_MESSAGE_STYLES.OTHER }) }) Hooks.on("hotbarDrop", (bar, data, slot) => { if (data.type === "Actor") { const actor = fromUuidSync(data.uuid) if (!actor) return false Macro.create({ name: actor.name, type: "script", img: actor.img, command: `game.actors.get("${actor.id}")?.sheet?.render(true)`, scope: "global", }).then(macro => bar.assignMacro(macro, slot)) return false } if (data.type === "Item") { const item = fromUuidSync(data.uuid) if (!item) return false const actor = item.parent let command = "" if (item.type === "competence") { command = `const a = game.actors.get("${actor?.id}"); const i = a?.items?.get("${item.id}"); if (i) a?.system?.roll?.("competence", i)` } else if (item.type === "sortilege") { command = `const { default: Cast } = await import("${game.system.id}/module/applications/sortilege-casting.mjs"); const a = game.actors.get("${actor?.id}"); const i = a?.items?.get("${item.id}"); if (i) Cast.prompt(i, a)` } else if (item.type === "arme") { command = `const a = game.actors.get("${actor?.id}"); const i = a?.items?.get("${item.id}"); if (i) a?.system?.roll?.("weapon", i)` } else { command = `const a = game.actors.get("${actor?.id}"); a?.items?.get("${item.id}")?.sheet?.render(true)` } if (command) { Macro.create({ name: item.name, type: "script", img: item.img, command, scope: "actor", }).then(macro => bar.assignMacro(macro, slot)) } return false } })