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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,31 @@ import * as models from "./models/index.mjs";
|
||||
import * as sheets from "./applications/sheets/_module.mjs";
|
||||
import { DonjonEtCieRollDialog } from "./applications/donjon-et-cie-roll-dialog.mjs";
|
||||
import { DonjonEtCieRolls } from "./donjon-et-cie-rolls.mjs";
|
||||
import { DonjonEtCieMacros } from "./donjon-et-cie-macros.mjs";
|
||||
|
||||
const WELCOME_MESSAGE_SETTING = "welcomeMessageVersion";
|
||||
|
||||
function injectActorDirectoryMissionPackButton(app, element) {
|
||||
if (!game.user.isGM) return;
|
||||
|
||||
const root = app?.element ?? element?.[0] ?? element;
|
||||
if (!(root instanceof HTMLElement)) return;
|
||||
|
||||
const headerActions = root.querySelector(".directory-header .header-actions");
|
||||
if (!(headerActions instanceof HTMLElement)) return;
|
||||
if (headerActions.querySelector(".dnc-mission-pack-button")) return;
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "dnc-mission-pack-button";
|
||||
button.title = game.i18n.localize("DNC.Macro.MissionPack.SidebarButton");
|
||||
button.setAttribute("aria-label", game.i18n.localize("DNC.Macro.MissionPack.SidebarButton"));
|
||||
button.innerHTML = `<i class="fa-solid fa-box-open" inert></i><span>${game.i18n.localize("DNC.Macro.MissionPack.SidebarButton")}</span>`;
|
||||
button.addEventListener("click", () => {
|
||||
void game.system.donjonEtCie.macros.openMissionPackDialog();
|
||||
});
|
||||
headerActions.append(button);
|
||||
}
|
||||
|
||||
function onChatActionClick(event) {
|
||||
const button = event.target.closest("[data-action='rollChatDamage'], [data-action='rollSpellChaos'], [data-action='applyDamage']");
|
||||
@@ -64,6 +89,73 @@ function onChatActionClick(event) {
|
||||
})();
|
||||
}
|
||||
|
||||
function registerSystemSettings() {
|
||||
game.settings.register("fvtt-donjon-et-cie", WELCOME_MESSAGE_SETTING, {
|
||||
name: "Version du message de bienvenue",
|
||||
hint: "Usage interne pour eviter de republier le message de bienvenue a chaque chargement.",
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: String,
|
||||
default: ""
|
||||
});
|
||||
}
|
||||
|
||||
async function getHelpJournalLink() {
|
||||
const pack = [...game.packs.values()].find((candidate) => candidate.metadata.name === "system-help");
|
||||
if (!pack) return null;
|
||||
|
||||
const index = await pack.getIndex();
|
||||
const entry = index.find((document) => document.name === "Aide du systeme");
|
||||
if (!entry?._id) return null;
|
||||
|
||||
const journal = await pack.getDocument(entry._id);
|
||||
if (!journal?.uuid) return null;
|
||||
|
||||
return `@UUID[${journal.uuid}]{${game.i18n.localize("DNC.Welcome.HelpLinkLabel")}}`;
|
||||
}
|
||||
|
||||
async function maybeCreateWelcomeMessage() {
|
||||
if (!game.user.isGM) return;
|
||||
|
||||
const currentVersion = String(game.system.version ?? "");
|
||||
const shownVersion = String(game.settings.get("fvtt-donjon-et-cie", WELCOME_MESSAGE_SETTING) ?? "");
|
||||
if (shownVersion === currentVersion) return;
|
||||
|
||||
const helpJournalLink = await getHelpJournalLink();
|
||||
const content = await foundry.applications.handlebars.renderTemplate(
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/welcome-card.hbs",
|
||||
{
|
||||
title: game.i18n.localize("DNC.Welcome.Title"),
|
||||
subtitle: game.i18n.format("DNC.Welcome.Subtitle", { version: currentVersion }),
|
||||
intro: game.i18n.localize("DNC.Welcome.Intro"),
|
||||
bullets: [
|
||||
game.i18n.localize("DNC.Welcome.BulletActors"),
|
||||
game.i18n.localize("DNC.Welcome.BulletItems"),
|
||||
game.i18n.localize("DNC.Welcome.BulletMissionPack")
|
||||
],
|
||||
helpLabel: game.i18n.localize("DNC.Welcome.HelpLabel"),
|
||||
helpLink: helpJournalLink,
|
||||
helpFallback: game.i18n.localize("DNC.Welcome.HelpFallback"),
|
||||
footer: game.i18n.localize("DNC.Welcome.Footer"),
|
||||
creditsLabel: game.i18n.localize("DNC.Welcome.CreditsLabel"),
|
||||
creditsText: game.i18n.localize("DNC.Welcome.CreditsText"),
|
||||
officialLabel: game.i18n.localize("DNC.Welcome.OfficialLabel"),
|
||||
officialUrl: "https://johndoe-rpg.com/catalogue/donjon-cie/",
|
||||
officialLinkText: game.i18n.localize("DNC.Welcome.OfficialLinkText")
|
||||
}
|
||||
);
|
||||
|
||||
await ChatMessage.create({
|
||||
speaker: {
|
||||
alias: game.system.title
|
||||
},
|
||||
user: game.user.id,
|
||||
content: await TextEditor.enrichHTML(content, { async: true })
|
||||
});
|
||||
|
||||
await game.settings.set("fvtt-donjon-et-cie", WELCOME_MESSAGE_SETTING, currentVersion);
|
||||
}
|
||||
|
||||
Hooks.once("init", async () => {
|
||||
const startupBanner =
|
||||
`▗▄▄▄ ▗▄▖ ▗▖ ▗▖ ▗▖ ▗▄▖ ▗▖ ▗▖ ▗▄▄▄▖▗▄▄▄▖ ▗▄▄▖▗▄▄▄▖▗▞▀▚▖
|
||||
@@ -75,6 +167,7 @@ Hooks.once("init", async () => {
|
||||
console.log(`%c${startupBanner}`, "font-family: monospace; white-space: pre; line-height: 1.1;");
|
||||
console.log("Initialisation du systeme Donjon & Cie");
|
||||
|
||||
registerSystemSettings();
|
||||
await DonjonEtCieUtility.preloadHandlebarsTemplates();
|
||||
|
||||
CONFIG.Combat.initiative = {
|
||||
@@ -107,7 +200,8 @@ Hooks.once("init", async () => {
|
||||
sheets,
|
||||
rolls: DonjonEtCieRolls,
|
||||
dialogs: DonjonEtCieRollDialog,
|
||||
utility: DonjonEtCieUtility
|
||||
utility: DonjonEtCieUtility,
|
||||
macros: DonjonEtCieMacros
|
||||
};
|
||||
|
||||
foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet);
|
||||
@@ -122,4 +216,9 @@ Hooks.once("init", async () => {
|
||||
|
||||
Hooks.once("ready", () => {
|
||||
document.addEventListener("click", onChatActionClick);
|
||||
void maybeCreateWelcomeMessage();
|
||||
});
|
||||
|
||||
Hooks.on("renderActorDirectory", (app, element) => {
|
||||
injectActorDirectoryMissionPackButton(app, element);
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ export class DonjonEtCieUtility {
|
||||
"systems/fvtt-donjon-et-cie/templates/dialogs/damage-roll.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/dialogs/spell-roll.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/dialogs/usage-roll.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/dialogs/mission-pack-dialog.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/roll-card.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/spell-card.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/chaos-card.hbs",
|
||||
@@ -43,7 +44,9 @@ export class DonjonEtCieUtility {
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/favor-card.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/initiative-card.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/usage-card.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/item-card.hbs"
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/item-card.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/mission-pack-card.hbs",
|
||||
"systems/fvtt-donjon-et-cie/templates/chat/welcome-card.hbs"
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user