Gestion de paquetage, aide intégrée et message de bienvenue
Release Creation / build (release) Successful in 59s

This commit is contained in:
2026-05-01 00:37:01 +02:00
parent 06b0ff7f78
commit 7d218f4a0a
41 changed files with 1524 additions and 54 deletions
+340
View File
@@ -0,0 +1,340 @@
/**
* 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
};
}
}