Files
fvtt-donjon-et-cie/modules/donjon-et-cie-macros.mjs
T
2026-05-01 00:37:01 +02:00

341 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 20252026 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<object|null>}
*/
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<object|null>}
*/
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
};
}
}