341 lines
11 KiB
JavaScript
341 lines
11 KiB
JavaScript
/**
|
||
* 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<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
|
||
};
|
||
}
|
||
}
|