Gestion de paquetage, aide intégrée et message de bienvenue
Release Creation / build (release) Successful in 59s
Release Creation / build (release) Successful in 59s
This commit is contained in:
@@ -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 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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user