From daf3d6953a1c1b96b066b53cca462d13a4831a74 Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Tue, 17 Dec 2024 14:51:58 -0800 Subject: [PATCH 1/2] [#4898] Add support for multiple abilities in single save enricher A single save enricher can now specify multiple abilities: ```html [[/save ability=strength/dexterity]] [[/save str dex]] ``` This will result in an enriched link for each ability as well as a request roll link that posts a single chat card with a button for each ability. --- module/enrichers.mjs | 120 +++++++++++++++++++++++++------- templates/chat/request-card.hbs | 8 ++- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/module/enrichers.mjs b/module/enrichers.mjs index c0eb82d4b1..ef0d061f0b 100644 --- a/module/enrichers.mjs +++ b/module/enrichers.mjs @@ -395,49 +395,96 @@ async function enrichCheck(config, label, options) { * ```[[/save ability=dex]]``` * becomes * ```html - * - * Dexterity - * + * + * Dexterity + * + * * ``` * * @example Add a DC to the save: * ```[[/save ability=dex dc=20]]``` * becomes * ```html - * - * DC 20 Dexterity - * + * + * DC 20 Dexterity + * + * + * ``` + * + * @example Specify multiple abilities: + * ```[[/save ability=str/dex dc=20]]``` + * ```[[/save strength dexterity dc=20]]``` + * becomes + * ```html + * + * DC 20 + * Strength or + * Dexterity + * + * * ``` * * @example Create a concentration check: * ```[[/concentration 10]]``` * becomes * ```html - * - * DC 10 concentration - * + * + * DC 10 concentration + * + * * ``` */ async function enrichSave(config, label, options) { + config.ability = config.ability?.replace("/", "|").split("|") ?? []; for ( let value of config.values ) { const slug = foundry.utils.getType(value) === "string" ? slugify(value) : value; if ( slug in CONFIG.DND5E.enrichmentLookup.abilities ) config.ability = slug; else if ( Number.isNumeric(value) ) config.dc = Number(value); else config[value] = true; } + config.ability = config.ability + .filter(a => a in CONFIG.DND5E.enrichmentLookup.abilities) + .map(a => CONFIG.DND5E.enrichmentLookup.abilities[a].key ?? a); - const abilityConfig = CONFIG.DND5E.enrichmentLookup.abilities[config.ability]; - if ( !abilityConfig && !config._isConcentration ) { - console.warn(`Ability ${config.ability} not found while enriching ${config._input}.`); + if ( !config.ability.length && !config._isConcentration ) { + console.warn(`No ability found while enriching ${config._input}.`); return null; } - if ( abilityConfig?.key ) config.ability = abilityConfig.key; if ( config.dc && !Number.isNumeric(config.dc) ) config.dc = simplifyBonus(config.dc, options.rollData); + if ( config.ability.length > 1 && label ) { + console.warn(`Multiple abilities and custom label found while enriching ${config._input}, which aren't supported together.`); + return null; + } + config = { type: config._isConcentration ? "concentration" : "save", ...config }; - if ( !label ) label = createRollLabel(config); - return createRequestLink(createRollLink(label), config); + if ( label ) label = createRollLink(label); + else if ( config.ability.length <= 1 ) label = createRollLink(createRollLabel(config)); + else { + label = game.i18n.getListFormatter({ type: "disjunction" }).format(config.ability.map(ability => + createRollLink(createRollLabel({ type: "save", ability }), { ability }).outerHTML + )); + if ( config.dc && !config.hideDC ) { + label = game.i18n.format("EDITOR.DND5E.Inline.DC", { dc: config.dc, check: label }); + } + label = game.i18n.format(`EDITOR.DND5E.Inline.Save${config.format === "long" ? "Long" : "Short"}`, { save: label }); + const template = document.createElement("template"); + template.innerHTML = label; + label = template; + } + return createRequestLink(label, { ...config, ability: config.ability.join("|") }); +} + +/* -------------------------------------------- */ + +/** + * Create the buttons for a save requested in chat. + * @param {object} dataset + * @returns {object[]} + */ +function createSaveRequestButtons(dataset) { + return (dataset.ability?.split("|") ?? []).map(ability => createRequestButton({ ...dataset, ability })); } /* -------------------------------------------- */ @@ -1010,7 +1057,7 @@ function createRequestLink(label, dataset) { const span = document.createElement("span"); span.classList.add("roll-link-group"); _addDataset(span, dataset); - if ( label instanceof HTMLElement ) span.insertAdjacentElement("afterbegin", label); + if ( label instanceof HTMLTemplateElement ) span.append(label.content); else span.append(label); // Add chat request link for GMs @@ -1096,21 +1143,26 @@ async function rollAction(event) { if ( !target ) return; event.stopPropagation(); - const { type, ability, skill, tool, dc } = target.dataset; + const dataset = { + ...((event.target.closest(".roll-link-group") ?? target)?.dataset ?? {}), + ...(event.target.closest(".roll-link")?.dataset ?? {}) + }; + const { type, ability, skill, tool, dc } = dataset; const options = { event }; if ( ability in CONFIG.DND5E.abilities ) options.ability = ability; - if ( dc ) options.target = dc; + if ( dc ) options.target = Number(dc); const action = event.target.closest("a")?.dataset.action ?? "roll"; + const link = event.target.closest("a") ?? event.target; // Direct roll if ( (action === "roll") || !game.user.isGM ) { - target.disabled = true; + link.disabled = true; try { switch ( type ) { case "attack": return await rollAttack(event); case "damage": return await rollDamage(event); - case "item": return await useItem(target.dataset); + case "item": return await useItem(dataset); } const actors = getSceneTargets().map(t => t.actor); @@ -1140,20 +1192,21 @@ async function rollAction(event) { } } } finally { - target.disabled = false; + link.disabled = false; } } // Roll request else { const MessageClass = getDocumentClass("ChatMessage"); + + let buttons; + if ( dataset.type === "save" ) buttons = createSaveRequestButtons(dataset); + else buttons = [createRequestButton(dataset)]; + const chatData = { user: game.user.id, - content: await renderTemplate("systems/dnd5e/templates/chat/request-card.hbs", { - buttonLabel: createRollLabel({ ...target.dataset, format: "short", icon: true }), - hiddenLabel: createRollLabel({ ...target.dataset, format: "short", icon: true, hideDC: true }), - dataset: { ...target.dataset, action: "rollRequest", visibility: "all" } - }), + content: await renderTemplate("systems/dnd5e/templates/chat/request-card.hbs", { buttons }), flavor: game.i18n.localize("EDITOR.DND5E.Inline.RollRequest"), speaker: MessageClass.getSpeaker({user: game.user}) }; @@ -1163,6 +1216,21 @@ async function rollAction(event) { /* -------------------------------------------- */ +/** + * Create a button for a chat request. + * @param {object} dataset + * @returns {object} + */ +function createRequestButton(dataset) { + return { + buttonLabel: createRollLabel({ ...dataset, format: "short", icon: true }), + hiddenLabel: createRollLabel({ ...dataset, format: "short", icon: true, hideDC: true }), + dataset: { ...dataset, action: "rollRequest", visibility: "all" } + }; +} + +/* -------------------------------------------- */ + /** * Perform an attack roll. * @param {Event} event The click event triggering the action. diff --git a/templates/chat/request-card.hbs b/templates/chat/request-card.hbs index 4155b2072f..4f0db946d9 100644 --- a/templates/chat/request-card.hbs +++ b/templates/chat/request-card.hbs @@ -1,8 +1,10 @@
- + {{/each}}
From 067e2db5888d036104540597ab44f9d4ba41d1ce Mon Sep 17 00:00:00 2001 From: Jeff Hitchcock Date: Fri, 3 Jan 2025 08:17:02 -0800 Subject: [PATCH 2/2] [#4898] Adjust comments, fix concentration challenge message --- module/documents/actor/actor.mjs | 8 +++++--- module/enrichers.mjs | 23 ++++++++++++----------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/module/documents/actor/actor.mjs b/module/documents/actor/actor.mjs index 0a671361e9..5f122dcf4b 100644 --- a/module/documents/actor/actor.mjs +++ b/module/documents/actor/actor.mjs @@ -1297,9 +1297,11 @@ export default class Actor5e extends SystemDocumentMixin(Actor) { return ChatMessage.implementation.create({ content: await renderTemplate("systems/dnd5e/templates/chat/request-card.hbs", { - dataset: { ...dataset, type: "concentration", visbility: "all" }, - buttonLabel: createRollLabel({ ...dataset, ...config }), - hiddenLabel: createRollLabel({ ...dataset, ...config, hideDC: true }) + buttons: [{ + dataset: { ...dataset, type: "concentration", visbility: "all" }, + buttonLabel: createRollLabel({ ...dataset, ...config }), + hiddenLabel: createRollLabel({ ...dataset, ...config, hideDC: true }) + }] }), whisper: game.users.filter(user => this.testUserPermission(user, "OWNER")), speaker: ChatMessage.implementation.getSpeaker({ actor: this }) diff --git a/module/enrichers.mjs b/module/enrichers.mjs index ef0d061f0b..dff4083677 100644 --- a/module/enrichers.mjs +++ b/module/enrichers.mjs @@ -396,7 +396,7 @@ async function enrichCheck(config, label, options) { * becomes * ```html * - * Dexterity + * Dexterity * * * ``` @@ -406,7 +406,7 @@ async function enrichCheck(config, label, options) { * becomes * ```html * - * DC 20 Dexterity + * DC 20 Dexterity * * * ``` @@ -418,18 +418,18 @@ async function enrichCheck(config, label, options) { * ```html * * DC 20 - * Strength or - * Dexterity + * Strength or + * Dexterity * * * ``` * - * @example Create a concentration check: + * @example Create a concentration saving throw: * ```[[/concentration 10]]``` * becomes * ```html * - * DC 10 concentration + * DC 10 concentration * * * ``` @@ -438,7 +438,7 @@ async function enrichSave(config, label, options) { config.ability = config.ability?.replace("/", "|").split("|") ?? []; for ( let value of config.values ) { const slug = foundry.utils.getType(value) === "string" ? slugify(value) : value; - if ( slug in CONFIG.DND5E.enrichmentLookup.abilities ) config.ability = slug; + if ( slug in CONFIG.DND5E.enrichmentLookup.abilities ) config.ability.push(slug); else if ( Number.isNumeric(value) ) config.dc = Number(value); else config[value] = true; } @@ -484,7 +484,8 @@ async function enrichSave(config, label, options) { * @returns {object[]} */ function createSaveRequestButtons(dataset) { - return (dataset.ability?.split("|") ?? []).map(ability => createRequestButton({ ...dataset, ability })); + return (dataset.ability?.split("|") ?? []) + .map(ability => createRequestButton({ ...dataset, format: "long", ability })); } /* -------------------------------------------- */ @@ -1202,7 +1203,7 @@ async function rollAction(event) { let buttons; if ( dataset.type === "save" ) buttons = createSaveRequestButtons(dataset); - else buttons = [createRequestButton(dataset)]; + else buttons = [createRequestButton({ ...dataset, format: "short" })]; const chatData = { user: game.user.id, @@ -1223,8 +1224,8 @@ async function rollAction(event) { */ function createRequestButton(dataset) { return { - buttonLabel: createRollLabel({ ...dataset, format: "short", icon: true }), - hiddenLabel: createRollLabel({ ...dataset, format: "short", icon: true, hideDC: true }), + buttonLabel: createRollLabel({ ...dataset, icon: true }), + hiddenLabel: createRollLabel({ ...dataset, icon: true, hideDC: true }), dataset: { ...dataset, action: "rollRequest", visibility: "all" } }; }