/** * 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_MODE_SETTING = "missionPackMode"; static MISSION_PACK_SOCKET_SCOPE = "missionPackCampaign"; static MISSION_PACK_REQUEST_TIMEOUT = 300000; static MISSION_PACK_CAMPAIGN_DICE = [20, 12, 10, 8]; static MISSION_PACK_STEWARD_MODES = { positive: "avantage", neutral: "normal", negative: "desavantage" }; static #campaignRequests = new Map(); static #socketRegistered = false; 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 MISSION_PACK_UNIQUE_ITEMS = [ { name: "L epee de Monsieur Noir", type: "arme", system: { categorie: "melee", caracteristique: "force", degats: "1d6", portee: "", mains: 1, equipee: false, description: "

Elle n a rien de particulier mais confere un prestige important.

Avantage a tous les jets d interaction avec les employes de Donjon & Cie.

", notes: "" } }, { name: "Fil a plomb d Arnezon", type: "equipement", system: { quantite: 1, equipee: false, emplacement: "", description: "

Il oscille de maniere clairement etrange en presence d un passage secret.

", notes: "" } }, { name: "Boussole de Drize Durban", type: "equipement", system: { quantite: 1, equipee: false, emplacement: "", description: "

Trois reglages : pointe vers le client, le resident ou l employe le plus proche.

", notes: "" } }, { name: "Boule de cristal de la supervision", type: "equipement", system: { quantite: 1, equipee: false, emplacement: "", description: "

Permet de voir toute personne ou lieu qu on connait deja.

Jet de SAG pour la controler. La plupart des huiles sentent quand on les observe.

", notes: "" } }, { name: "Dent du dragon Leogradonardicus III", type: "equipement", system: { quantite: 1, equipee: false, emplacement: "", description: "

Un jet de CHA reussi permet de controler les reptiles inintelligents et de charmer les humanoides reptiliens.

", notes: "" } }, { name: "Doigt d Aarcarcerax", type: "consommable", system: { quantite: 1, delta: 4, effet: "Tue la creature vers qui on pointe le doigt.", description: "

Tue la creature vers qui on pointe le doigt.

La liche sait instantanement qu on a retrouve son doigt.

", notes: "" } }, { name: "Cape de Vlad von Drakovitch", type: "consommable", system: { quantite: 1, delta: 6, effet: "Permet de se transformer en 1-3 rats, 4-5 chauve-souris, 6 forme gazeuse.", description: "

Delta 6 charges.

Permet de se transformer en 1-3 rats, 4-5 chauve-souris, 6 forme gazeuse.

", notes: "" } }, { name: "Vieux carnet de notes d Affalella", type: "consommable", system: { quantite: 1, delta: 6, effet: "Fonctionne comme des faveurs de la Mercatique utilisables uniquement dans les aires client.", description: "

Vieux carnet de notes d Affalella, directrice de la Mercatique.

Fonctionne comme Delta 6 faveurs de la Mercatique utilisables uniquement dans les aires client.

", notes: "" } }, { name: "Ancienne cle universelle de Paiji", type: "consommable", system: { quantite: 1, delta: 8, effet: "Ouvre tous les coffres, portes, armoires et autres serrures du Donjon.", description: "

Ancienne cle universelle de Paiji, directeur de la Maintenance.

Delta 8 usages. Ouvre tous les coffres, portes, armoires et autres serrures du Donjon.

", notes: "" } }, { name: "Baguette d urgence", type: "equipement", system: { quantite: 1, equipee: false, emplacement: "", description: "

Teleporte l utilisateur au palier des huiles lorsque la baguette est brisee.

", notes: "" } } ]; static registerSocketListeners() { if (this.#socketRegistered || !game.socket) return; game.socket.on(`system.${game.system.id}`, (payload) => { void this.#handleSocketMessage(payload); }); this.#socketRegistered = 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 #getModeLabel(mode) { if (mode === "avantage") return game.i18n.localize("DNC.UI.ModeAdvantage"); if (mode === "desavantage") return game.i18n.localize("DNC.UI.ModeDisadvantage"); return game.i18n.localize("DNC.UI.ModeNormal"); } static #getMissionPackMode() { return String(game.settings.get("fvtt-donjon-et-cie", this.MISSION_PACK_MODE_SETTING) ?? "classic"); } static #getMissionPackModeLabel(mode) { return game.i18n.localize(mode === "campaign" ? "DNC.Macro.MissionPack.ModeCampaign" : "DNC.Macro.MissionPack.ModeClassic"); } static #getMissionPackModeDescription(mode) { return game.i18n.localize(mode === "campaign" ? "DNC.Macro.MissionPack.ModeCampaignHint" : "DNC.Macro.MissionPack.ModeClassicHint"); } static #getStewardRelationLabel(relation) { return game.i18n.localize(`DNC.Macro.MissionPack.Relation.${relation ?? "neutral"}`); } static #getStewardRelationOptions() { return ["positive", "neutral", "negative"].map((relation) => ({ value: relation, label: this.#getStewardRelationLabel(relation) })); } static #getCampaignDiceOptions() { return this.MISSION_PACK_CAMPAIGN_DICE.map((sides) => ({ value: String(sides), label: `1d${sides}` })); } static #getDefaultCampaignAssignments() { return Object.fromEntries(this.MISSION_PACK_TABLES.map((spec, index) => [spec.key, String(this.MISSION_PACK_CAMPAIGN_DICE[index])])); } 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 #getMissionPackOwnerUsers(actor) { return game.users .filter((user) => !user.isGM && actor.testUserPermission(user, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) .sort((a, b) => Number(b.active) - Number(a.active) || a.name.localeCompare(b.name, "fr", { sensitivity: "base" })); } static #getCampaignTargetUser(actor) { return this.#getMissionPackOwnerUsers(actor).find((user) => user.active) ?? null; } 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 #evaluateRoll(formula) { const roll = await (new Roll(formula)).evaluate(); const values = roll.dice .flatMap((die) => die.results.map((result) => Number(result.result ?? result.count ?? 0))) .filter((value) => Number.isFinite(value)); return { roll, total: Number(roll.total ?? 0), values }; } static #formatRollValues(values, kept) { if (!values.length) return String(kept ?? 0); if (values.length === 1) return String(values[0]); return `${values.join(" / ")} -> ${kept}`; } static async #rollPackDie(sides, { mode = "normal" } = {}) { const formula = mode === "avantage" ? `2d${sides}kh` : mode === "desavantage" ? `2d${sides}kl` : `1d${sides}`; const evaluated = await this.#evaluateRoll(formula); return { sides: Number(sides), dieLabel: `1d${sides}`, roll: evaluated.roll, values: evaluated.values, valuesLabel: this.#formatRollValues(evaluated.values, evaluated.total), kept: evaluated.total, mode, modeLabel: this.#getModeLabel(mode) }; } static #createInlineItemData(spec) { return { name: spec.name, type: spec.type, img: DonjonEtCieUtility.getDefaultItemIcon(spec.type), system: foundry.utils.deepClone(spec.system) }; } static #getTableResults(table) { return table?.results?.contents ?? []; } static #getTableRange(result) { const rawRange = Array.isArray(result?.range) ? result.range : result?._source?.range ?? []; return [Number(rawRange[0] ?? 0), Number(rawRange[1] ?? 0)]; } static #findTableResultByTotal(table, total) { const entries = this.#getTableResults(table) .map((result) => ({ result, range: this.#getTableRange(result) })) .filter(({ range }) => Number.isFinite(range[0]) && Number.isFinite(range[1]) && range[1] >= range[0]) .sort((a, b) => a.range[0] - b.range[0]); if (!entries.length) { return { result: null, clampedTotal: total, range: null }; } const minimum = entries[0].range[0]; const maximum = entries.at(-1).range[1]; const clampedTotal = Math.max(minimum, Math.min(maximum, total)); const match = entries.find(({ range }) => clampedTotal >= range[0] && clampedTotal <= range[1]) ?? entries.at(-1); return { result: match.result, clampedTotal, range: match.range }; } static async #resolveUniqueMissionPackEntry({ comparisonRoll = null } = {}) { const reference = await this.#rollPackDie(20); const actorEvaluation = comparisonRoll == null ? await this.#rollPackDie(20) : null; const actorRoll = Number(comparisonRoll ?? actorEvaluation?.kept ?? 0); if (reference.kept !== actorRoll) { return { matched: false, referenceRoll: reference.kept, actorRoll, uniqueRoll: null, itemName: "", itemData: null, rolls: [reference.roll, ...(actorEvaluation ? [actorEvaluation.roll] : [])] }; } const uniqueRoll = await this.#rollPackDie(this.MISSION_PACK_UNIQUE_ITEMS.length); const spec = this.MISSION_PACK_UNIQUE_ITEMS[Math.max(0, uniqueRoll.kept - 1)] ?? this.MISSION_PACK_UNIQUE_ITEMS[0]; const document = await this.#findItemByName(spec.name); const itemData = document ? this.#toEmbeddedItemData(document) : this.#createInlineItemData(spec); return { matched: true, referenceRoll: reference.kept, actorRoll, uniqueRoll: uniqueRoll.kept, itemName: document?.name ?? spec.name, itemData, rolls: [reference.roll, ...(actorEvaluation ? [actorEvaluation.roll] : []), uniqueRoll.roll] }; } static async #resolveTableResultEntries(result, { multiple = false } = {}) { if (!result) { return { display: game.i18n.localize("DNC.Macro.MissionPack.NoResult"), entries: [] }; } const source = result._source ?? {}; const resultName = String(result.name ?? source.name ?? "").trim(); const resultDescription = String(result.description ?? source.description ?? "").trim(); const resultText = String(source.text ?? "").trim(); const resultLabel = resultName || resultDescription || resultText; const documentUuid = result.type === "document" ? result.documentUuid ?? source.documentUuid ?? null : null; if (documentUuid) { const document = await fromUuid(documentUuid); const label = document?.name ?? resultLabel; return { display: label, entries: label ? [{ name: label, document }] : [] }; } const sourceText = resultDescription || resultText || resultLabel; const uuidTargets = this.#extractUuidTargets(sourceText); 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(sourceText || resultLabel) : [resultLabel].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; } static async #materializeMissionPackEntries(entries) { const embeddedItems = []; const addedNames = []; const missingNames = []; for (const entry of entries) { const item = entry.document ?? await this.#findItemByName(entry.name); if (!item) { missingNames.push(entry.name); continue; } embeddedItems.push(this.#toEmbeddedItemData(item)); addedNames.push(item.name); } return { embeddedItems, addedNames, missingNames }; } static async #openCampaignAllocationDialog(actor, { playerName = "", requesterName = "" } = {}) { const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-campaign-dialog.hbs", { actorName: actor.name, playerName, requesterName, rank: Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0), assignments: this.MISSION_PACK_TABLES.map((spec) => ({ key: spec.key, label: this.#getMissionPackLabel(spec.key), fieldName: `${spec.key}Die`, selectedDie: this.#getDefaultCampaignAssignments()[spec.key] })), diceOptions: this.#getCampaignDiceOptions(), relationOptions: this.#getStewardRelationOptions(), selectedRelation: "neutral" } ); return foundry.applications.api.DialogV2.wait({ window: { title: game.i18n.localize("DNC.Macro.MissionPack.CampaignDialogTitle"), icon: "fa-solid fa-dice" }, classes: ["dnc-roll-dialog", "dnc-mission-pack-dialog"], content, modal: true, buttons: [ { action: "confirm", label: game.i18n.localize("DNC.Macro.MissionPack.CampaignDialogAction"), icon: "fa-solid fa-check", default: true, callback: async (event, button) => { const assignments = Object.fromEntries(this.MISSION_PACK_TABLES.map((spec) => [ spec.key, Number(button.form.elements[`${spec.key}Die`]?.value ?? 0) ])); const selectedDice = Object.values(assignments).filter((value) => value > 0); if (selectedDice.length !== this.MISSION_PACK_TABLES.length) { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnDiceRequired")); return null; } if (new Set(selectedDice).size !== selectedDice.length) { ui.notifications.warn(game.i18n.localize("DNC.Macro.MissionPack.WarnDiceUnique")); return null; } return { assignments, stewardRelation: button.form.elements.stewardRelation?.value ?? "neutral" }; } } ], rejectClose: false }); } static async #handleSocketMessage(payload) { if (payload?.scope !== this.MISSION_PACK_SOCKET_SCOPE) return; if (payload.type === "campaign-response") { const pending = this.#campaignRequests.get(payload.requestId); if (!pending) return; clearTimeout(pending.timeoutId); this.#campaignRequests.delete(payload.requestId); pending.resolve(payload.result ?? null); return; } if (payload.type !== "campaign-request" || payload.targetUserId !== game.user.id) return; const actor = await fromUuid(payload.actorUuid); const allocation = actor ? await this.#openCampaignAllocationDialog(actor, { playerName: game.user.name, requesterName: payload.requesterName ?? "" }) : null; game.socket.emit(`system.${game.system.id}`, { scope: this.MISSION_PACK_SOCKET_SCOPE, type: "campaign-response", requestId: payload.requestId, result: allocation ? { allocation, responderUserId: game.user.id, responderName: game.user.name } : { allocation: null, responderUserId: game.user.id, responderName: game.user.name } }); } static async #requestCampaignAllocation(actor, targetUser) { if (!targetUser || targetUser.id === game.user.id) { const allocation = await this.#openCampaignAllocationDialog(actor, { playerName: game.user.name, requesterName: game.user.name }); return allocation ? { allocation, responderName: game.user.name } : null; } const requestId = foundry.utils.randomID(); const responsePromise = new Promise((resolve) => { const timeoutId = globalThis.setTimeout(() => { this.#campaignRequests.delete(requestId); resolve(null); }, this.MISSION_PACK_REQUEST_TIMEOUT); this.#campaignRequests.set(requestId, { resolve, timeoutId }); }); game.socket.emit(`system.${game.system.id}`, { scope: this.MISSION_PACK_SOCKET_SCOPE, type: "campaign-request", requestId, targetUserId: targetUser.id, actorUuid: actor.uuid, requesterName: game.user.name }); const response = await responsePromise; if (!response?.allocation) { ui.notifications.warn(game.i18n.format("DNC.Macro.MissionPack.CampaignRequestCanceled", { actor: actor.name, player: targetUser.name })); return null; } return { allocation: response.allocation, responderName: response.responderName ?? targetUser.name }; } static async #finalizeMissionPack(actor, drawPlans, { generationMode = "classic", controllerName = "", stewardRelation = "neutral", uniqueComparisonRoll = null } = {}) { const draws = []; const embeddedItems = []; const rolls = []; let missingCount = 0; for (const plan of drawPlans) { rolls.push(...(plan.rolls ?? []).filter((roll) => roll instanceof Roll)); if (plan.failed || !plan.resolved) { draws.push({ label: this.#getMissionPackLabel(plan.spec.key), display: game.i18n.format("DNC.Macro.MissionPack.TableMissing", { table: plan.spec.name }), addedNames: [], addedSummary: "", missingNames: [], missingSummary: "", failed: true }); continue; } const materialized = await this.#materializeMissionPackEntries(plan.resolved.entries); embeddedItems.push(...materialized.embeddedItems); missingCount += materialized.missingNames.length; draws.push({ label: this.#getMissionPackLabel(plan.spec.key), display: plan.resolved.display || game.i18n.localize("DNC.Macro.MissionPack.NoResult"), addedNames: materialized.addedNames, addedSummary: materialized.addedNames.join(", "), missingNames: materialized.missingNames, missingSummary: materialized.missingNames.join(", "), failed: false, ...plan.detail }); } const uniqueEntry = await this.#resolveUniqueMissionPackEntry({ comparisonRoll: uniqueComparisonRoll }); rolls.push(...(uniqueEntry.rolls ?? []).filter((roll) => roll instanceof Roll)); if (uniqueEntry.itemData) { embeddedItems.push(uniqueEntry.itemData); } 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, uniqueEntry, isCampaign: generationMode === "campaign", generationModeLabel: this.#getMissionPackModeLabel(generationMode), controllerName, stewardRelationLabel: generationMode === "campaign" ? this.#getStewardRelationLabel(stewardRelation) : "" } ); await ChatMessage.create({ speaker: ChatMessage.getSpeaker({ actor }), user: game.user.id, content, rolls }); 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, uniqueEntry, generationMode, controllerName, stewardRelation }; } static async #grantClassicMissionPack(actor) { const drawPlans = []; for (const spec of this.MISSION_PACK_TABLES) { const table = await this.#findRollTableByName(spec.name); if (!table) { drawPlans.push({ spec, failed: true, resolved: null, rolls: [] }); continue; } const draw = await table.draw({ displayChat: false }); const result = draw.results?.[0] ?? null; drawPlans.push({ spec, failed: false, resolved: await this.#resolveTableResultEntries(result, { multiple: spec.multiple }), detail: null, rolls: draw.roll instanceof Roll ? [draw.roll] : [] }); } return this.#finalizeMissionPack(actor, drawPlans, { generationMode: "classic" }); } static async #grantCampaignMissionPack(actor) { const targetUser = this.#getCampaignTargetUser(actor); const allocationResult = await this.#requestCampaignAllocation(actor, targetUser); if (!allocationResult?.allocation) return null; const rankBonus = Number(actor.system.anciennete?.rang ?? actor.system.sante?.dv ?? 0); const stewardRelation = allocationResult.allocation.stewardRelation ?? "neutral"; const mode = this.MISSION_PACK_STEWARD_MODES[stewardRelation] ?? "normal"; const drawPlans = []; let uniqueComparisonRoll = null; for (const spec of this.MISSION_PACK_TABLES) { const table = await this.#findRollTableByName(spec.name); if (!table) { drawPlans.push({ spec, failed: true, resolved: null, rolls: [] }); continue; } const sides = Number(allocationResult.allocation.assignments?.[spec.key] ?? 0); const rollData = await this.#rollPackDie(sides, { mode }); if (sides === 20) uniqueComparisonRoll = rollData.kept; const total = rollData.kept + rankBonus; const tableResult = this.#findTableResultByTotal(table, total); drawPlans.push({ spec, failed: false, resolved: await this.#resolveTableResultEntries(tableResult.result, { multiple: spec.multiple }), detail: { dieLabel: rollData.dieLabel, modeLabel: rollData.modeLabel, rollValuesLabel: rollData.valuesLabel, kept: rollData.kept, rankBonus, total, resolvedTotal: tableResult.clampedTotal, clamped: tableResult.clampedTotal !== total }, rolls: [rollData.roll] }); } return this.#finalizeMissionPack(actor, drawPlans, { generationMode: "campaign", controllerName: allocationResult.responderName ?? targetUser?.name ?? game.user.name, stewardRelation, uniqueComparisonRoll }); } /** * 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 mode = this.#getMissionPackMode(); 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, modeLabel: this.#getMissionPackModeLabel(mode), modeDescription: this.#getMissionPackModeDescription(mode), isCampaign: mode === "campaign" } ); 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; } return this.#getMissionPackMode() === "campaign" ? this.#grantCampaignMissionPack(actor) : this.#grantClassicMissionPack(actor); } }