/** * Donjon & Cie - Systeme FoundryVTT * * Donjon & Cie est un jeu de role edite par John Doe. * Ce systeme FoundryVTT est une implementation independante et n'est pas * affilie a John Doe. * * @author LeRatierBretonnien * @copyright 2025–2026 LeRatierBretonnien * @license CC BY-NC-SA 4.0 – https://creativecommons.org/licenses/by-nc-sa/4.0/ */ import { DonjonEtCieUtility } from "./donjon-et-cie-utility.mjs"; export class DonjonEtCieMacros { static MISSION_PACK_TABLES = [ { key: "melee", name: "Armes de corps a corps", multiple: false }, { key: "ranged", name: "Armes a distance", multiple: false }, { key: "armor", name: "Armures", multiple: false }, { key: "misc", name: "Encas et equipement divers", multiple: true } ]; static #normalizeName(value) { return String(value ?? "") .normalize("NFD") .replace(/\p{Diacritic}/gu, "") .replace(/['’]/g, " ") .replace(/\s+/g, " ") .trim() .toLowerCase(); } static #getMissionPackLabel(key) { return game.i18n.localize(`DNC.Macro.MissionPack.${key}`); } static #getDefaultMissionPackActorId(actors) { const controlledActor = canvas?.tokens?.controlled?.[0]?.actor ?? null; if (controlledActor?.type === "employe") return controlledActor.id; return actors[0]?.id ?? ""; } static #getMissionPackActorOptions() { return game.actors .filter((actor) => actor.type === "employe") .sort((a, b) => a.name.localeCompare(b.name, "fr", { sensitivity: "base" })) .map((actor) => ({ value: actor.id, label: actor.name })); } static async #resolveMissionPackActor(target = null) { if (target?.documentName === "Actor") return target; if (target?.actor?.documentName === "Actor") return target.actor; if (typeof target === "string" && target) { const document = await fromUuid(target); if (document?.documentName === "Actor") return document; if (document?.actor?.documentName === "Actor") return document.actor; } const controlledTokens = canvas?.tokens?.controlled ?? []; if (controlledTokens.length === 1 && controlledTokens[0]?.actor) return controlledTokens[0].actor; if (controlledTokens.length > 1) { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnMultipleTokens")); return null; } return game.user.character ?? null; } static async #getRandomTableDocuments() { const worldTables = game.tables?.contents ?? []; if (worldTables.length) return worldTables; const pack = game.packs.get("fvtt-donjon-et-cie.random-tables"); return pack ? pack.getDocuments() : []; } static async #findRollTableByName(name) { const normalizedName = this.#normalizeName(name); const worldTable = game.tables.find((table) => this.#normalizeName(table.name) === normalizedName); if (worldTable) return worldTable; const packTables = await this.#getRandomTableDocuments(); return packTables.find((table) => this.#normalizeName(table.name) === normalizedName) ?? null; } static async #getEquipmentDocuments() { const pack = game.packs.get("fvtt-donjon-et-cie.equipment"); const packDocuments = pack ? await pack.getDocuments() : []; return [...(game.items?.contents ?? []), ...packDocuments]; } static async #findItemByName(name) { const normalizedName = this.#normalizeName(name); const documents = await this.#getEquipmentDocuments(); return documents.find((item) => this.#normalizeName(item.name) === normalizedName) ?? null; } static #extractUuidTargets(text) { const matches = [...String(text ?? "").matchAll(/@UUID\[([^\]]+)\](?:\{([^}]+)\})?/g)]; return matches.map((match) => ({ uuid: match[1], label: match[2] ?? "" })); } static #extractPlainTextEntries(text) { const rawText = String(text ?? "") .replace(/<[^>]+>/g, " ") .replace(/\s+/g, " ") .trim(); if (!rawText) return []; return rawText .split(",") .map((entry) => entry.trim()) .filter(Boolean) .filter((entry) => !/^dotation\s+\d+$/i.test(entry)); } static async #resolveTableResultEntries(result, { multiple = false } = {}) { if (!result) { return { display: game.i18n.localize("DNC.Macro.MissionPack.NoResult"), entries: [] }; } if (result.type === "document" && result.documentCollection && result.documentId) { const uuid = result.documentCollection.includes(".") ? `Compendium.${result.documentCollection}.Item.${result.documentId}` : `Item.${result.documentId}`; const document = await fromUuid(uuid); const label = document?.name ?? result.text ?? ""; return { display: label, entries: label ? [{ name: label, document }] : [] }; } const uuidTargets = this.#extractUuidTargets(result.text); if (uuidTargets.length) { const entries = []; for (const target of uuidTargets) { const document = await fromUuid(target.uuid); const name = document?.name ?? target.label; if (!name) continue; entries.push({ name, document }); } return { display: entries.map((entry) => entry.name).join(", "), entries }; } const names = multiple ? this.#extractPlainTextEntries(result.text) : [result.text].map((entry) => String(entry ?? "").trim()).filter(Boolean); return { display: names.join(", "), entries: names.map((name) => ({ name, document: null })) }; } static #toEmbeddedItemData(item) { const data = foundry.utils.deepClone(item.toObject()); delete data._id; delete data.folder; delete data.sort; return data; } /** * Open the GM-only mission pack dialog. * @returns {Promise} */ static async openMissionPackDialog() { if (!game.user.isGM) { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnGMOnly")); return null; } const actorOptions = this.#getMissionPackActorOptions(); if (!actorOptions.length) { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnNoEmployees")); return null; } const selectedActorId = this.#getDefaultMissionPackActorId(actorOptions.map((option) => game.actors.get(option.value)).filter(Boolean)); const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-dialog.hbs", { actorOptions, selectedActorId } ); return foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.localize("DNC.Macro.MissionPack.DialogTitle"), icon: "fa-solid fa-box-open" }, classes: ["dnc-roll-dialog"], content, modal: false, buttons: [ { action: "grant", label: game.i18n.localize("DNC.Macro.MissionPack.DialogAction"), icon: "fa-solid fa-box-open", default: true, callback: async (event, button) => { const actorId = button.form.elements.actorId?.value ?? ""; const actor = actorId ? game.actors.get(actorId) : null; if (!actor) { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnNoActor")); return null; } return this.grantMissionPack(actor); } } ], rejectClose: false }); } /** * Draw the initial mission pack for the resolved actor and add the resulting items. * @param {Actor|string|null} target Resolved actor, token, or UUID. Defaults to selected token or user character. * @returns {Promise} */ static async grantMissionPack(target = null) { const actor = await this.#resolveMissionPackActor(target); if (!actor) { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnNoActor")); return null; } if (actor.type !== "employe") { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnInvalidActor")); return null; } const draws = []; const embeddedItems = []; let missingCount = 0; for (const spec of this.MISSION_PACK_TABLES) { const table = await this.#findRollTableByName(spec.name); if (!table) { draws.push({ label: this.#getMissionPackLabel(spec.key), display: game.i18n.format("DNC.Macro.MissionPack.TableMissing", { table: spec.name }), addedNames: [], addedSummary: "", missingNames: [], missingSummary: "", failed: true }); continue; } const draw = await table.draw({ displayChat: false }); const result = draw.results?.[0] ?? null; const resolved = await this.#resolveTableResultEntries(result, { multiple: spec.multiple }); const addedNames = []; const missingNames = []; for (const entry of resolved.entries) { const item = entry.document ?? await this.#findItemByName(entry.name); if (!item) { missingNames.push(entry.name); missingCount += 1; continue; } embeddedItems.push(this.#toEmbeddedItemData(item)); addedNames.push(item.name); } draws.push({ label: this.#getMissionPackLabel(spec.key), display: resolved.display || game.i18n.localize("DNC.Macro.MissionPack.NoResult"), addedNames, addedSummary: addedNames.join(", "), missingNames, missingSummary: missingNames.join(", "), failed: false }); } const createdItems = embeddedItems.length ? await actor.createEmbeddedDocuments("Item", embeddedItems, { renderSheet: false }) : []; const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-donjon-et-cie/templates/chat/mission-pack-card.hbs", { title: game.i18n.localize("DNC.Macro.MissionPack.Title"), actorName: actor.name, createdCount: createdItems.length, missingCount, draws } ); await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor }), user: game.user.id, content }); if (createdItems.length && !missingCount) { ui.notifications.info(game.i18n.format("DNC.Macro.MissionPack.Success", { actor: actor.name, count: createdItems.length })); } else if (createdItems.length) { ui.notifications.warn(game.i18n.format("DNC.Macro.MissionPack.Partial", { actor: actor.name, count: createdItems.length, missing: missingCount })); } else { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnNothingAdded")); } return { actor, createdItems, missingCount, draws }; } }