diff --git a/module/enrichers.mjs b/module/enrichers.mjs
index dff4083677..98932a0714 100644
--- a/module/enrichers.mjs
+++ b/module/enrichers.mjs
@@ -291,7 +291,7 @@ async function enrichAward(config, label, options) {
* becomes
* ```html
*
- * Dexterity check
+ * Dexterity check
*
* ```
*
@@ -300,7 +300,7 @@ async function enrichAward(config, label, options) {
* becomes
* ```html
*
- * DC 20 Dexterity (Acrobatics) check
+ * DC 20 Dexterity (Acrobatics) check
*
* ```
*
@@ -309,7 +309,7 @@ async function enrichAward(config, label, options) {
* becomes
* ```html
*
- * Strength (Acrobatics) check
+ * Strength (Acrobatics) check
*
* ```
*
@@ -318,7 +318,7 @@ async function enrichAward(config, label, options) {
* becomes
* ```html
*
- * Intelligence (Thieves' Tools) check
+ * Intelligence (Thieves' Tools) check
*
* ```
*
@@ -327,63 +327,178 @@ async function enrichAward(config, label, options) {
* becomes
* ```html
*
- * DC 15 Charisma check
+ * DC 15 Charisma check
*
* ```
+ *
+ * @example Use multiple skills in a check using default abilities:
+ * ```[[/check skill=acr/ath dc=15]]```
+ * ```[[/check acrobatics athletics 15]]```
+ * becomes
+ * ```html
+ *
+ * DC 15
+ *
+ * Dexterity (Acrobatics)
+ * or
+ *
+ * Strength (Athletics)
+ *
+ *
+ *
+ * ```
+ *
+ * @example Use multiple skills with a fixed ability:
+ * ```[[/check ability=str skill=dec/per dc=15]]```
+ * ```[[/check strength deception persuasion 15]]```
+ * becomes
+ * ```html
+ *
+ * DC 15 Strength
+ * ( Deception or
+ * Persuasion)
+ *
+ *
+ * ```
*/
async function enrichCheck(config, label, options) {
+ config.skill = config.skill?.replaceAll("/", "|").split("|") ?? [];
+ config.tool = config.tool?.replaceAll("/", "|").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 ( slug in CONFIG.DND5E.enrichmentLookup.skills ) config.skill = slug;
- else if ( slug in CONFIG.DND5E.enrichmentLookup.tools ) config.tool = slug;
+ else if ( slug in CONFIG.DND5E.enrichmentLookup.skills ) config.skill.push(slug);
+ else if ( slug in CONFIG.DND5E.enrichmentLookup.tools ) config.tool.push(slug);
else if ( Number.isNumeric(value) ) config.dc = Number(value);
else config[value] = true;
}
+ delete config.values;
+ const groups = new Map();
let invalid = false;
- const skillConfig = CONFIG.DND5E.enrichmentLookup.skills[slugify(config.skill)];
- if ( config.skill && !skillConfig ) {
- console.warn(`Skill ${config.skill} not found while enriching ${config._input}.`);
+ // TODO: Support "spellcasting" ability
+ let abilityConfig = CONFIG.DND5E.enrichmentLookup.abilities[slugify(config.ability)];
+ if ( config.ability && !abilityConfig ) {
+ console.warn(`Ability "${config.ability}" not found while enriching ${config._input}.`);
invalid = true;
- } else if ( config.skill && !config.ability ) {
- config.ability = skillConfig.ability;
+ } else if ( abilityConfig?.key ) config.ability = abilityConfig.key;
+
+ for ( let [index, skill] of config.skill.entries() ) {
+ const skillConfig = CONFIG.DND5E.enrichmentLookup.skills[slugify(skill)];
+ if ( skillConfig ) {
+ if ( skillConfig.key ) skill = config.skill[index] = skillConfig.key;
+ const ability = config.ability || skillConfig.ability;
+ if ( !groups.has(ability) ) groups.set(ability, []);
+ groups.get(ability).push({ key: skill, type: "skill", label: skillConfig.label });
+ } else {
+ console.warn(`Skill "${skill}" not found while enriching ${config._input}.`);
+ invalid = true;
+ }
}
- if ( skillConfig?.key ) config.skill = skillConfig.key;
- const toolConfig = CONFIG.DND5E.tools[slugify(config.tool)];
- const toolUUID = CONFIG.DND5E.enrichmentLookup.tools[slugify(config.tool)];
- const toolIndex = toolUUID ? Trait.getBaseItem(toolUUID, { indexOnly: true }) : null;
- if ( config.tool && !toolIndex ) {
- console.warn(`Tool ${config.tool} not found while enriching ${config._input}.`);
+ for ( const tool of config.tool ) {
+ const toolConfig = CONFIG.DND5E.tools[slugify(tool)];
+ const toolUUID = CONFIG.DND5E.enrichmentLookup.tools[slugify(tool)];
+ const toolIndex = toolUUID ? Trait.getBaseItem(toolUUID, { indexOnly: true }) : null;
+ if ( toolIndex ) {
+ const ability = config.ability || toolConfig?.ability;
+ if ( ability ) {
+ if ( !groups.has(ability) ) groups.set(ability, []);
+ groups.get(ability).push({ key: tool, type: "tool", label: toolIndex.name });
+ } else {
+ console.warn(`Tool "${tool}" found without specified or default ability while enriching ${config._input}.`);
+ invalid = true;
+ }
+ } else {
+ console.warn(`Tool "${tool}" not found while enriching ${config._input}.`);
+ invalid = true;
+ }
+ }
+
+ if ( !abilityConfig && !groups.size ) {
+ console.warn(`No ability provided while enriching ${config._input}.`);
invalid = true;
- } else if ( config.tool && !config.ability && toolConfig ) {
- config.ability = toolConfig.ability;
}
- let abilityConfig = CONFIG.DND5E.enrichmentLookup.abilities[slugify(config.ability)];
- if ( config.ability && !abilityConfig ) {
- console.warn(`Ability ${config.ability} not found while enriching ${config._input}.`);
+ const complex = (config.skill.length + config.tool.length) > 1;
+ if ( config.passive && complex ) {
+ console.warn(`Multiple skills or tools and passive flag found while enriching ${config._input}, which aren't supported together.`);
invalid = true;
- } else if ( !abilityConfig ) {
- console.warn(`No ability provided while enriching check ${config._input}.`);
+ }
+ if ( label && complex ) {
+ console.warn(`Multiple skills or tools and a custom label found while enriching ${config._input}, which aren't supported together.`);
invalid = true;
}
- if ( abilityConfig?.key ) config.ability = abilityConfig.key;
if ( config.dc && !Number.isNumeric(config.dc) ) config.dc = simplifyBonus(config.dc, options.rollData);
if ( invalid ) return null;
- const type = config.skill ? "skill" : config.tool ? "tool" : "check";
- config = { type, ...config };
+ if ( complex ) {
+ const formatter = game.i18n.getListFormatter({ type: "disjunction" });
+ const parts = [];
+ for ( const [ability, associated] of groups.entries() ) {
+ const makeConfig = ({ key, type }) => ({ type, [type]: key, ability: groups.size > 1 ? ability : undefined });
+
+ // Multiple associated proficiencies, link each individually
+ if ( associated.length > 1 ) parts.push(
+ game.i18n.format("EDITOR.DND5E.Inline.SpecificCheck", {
+ ability: CONFIG.DND5E.enrichmentLookup.abilities[ability].label,
+ type: formatter.format(associated.map(a => createRollLink(a.label, makeConfig(a)).outerHTML ))
+ })
+ );
+
+ // Only single associated proficiency, wrap whole thing in roll link
+ else {
+ const associatedConfig = makeConfig(associated[0]);
+ parts.push(createRollLink(createRollLabel({ ...associatedConfig, ability }), associatedConfig).outerHTML);
+ }
+ }
+ label = formatter.format(parts);
+ 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.Check${config.format === "long" ? "Long" : "Short"}`, { check: label });
+ const template = document.createElement("template");
+ template.innerHTML = label;
+ return createRequestLink(template, {
+ type: "check", ...config, skill: config.skill.join("|"), tool: config.tool.join("|")
+ });
+ }
+
+ const type = config.skill.length ? "skill" : config.tool.length ? "tool" : "check";
+ config = { type, ability: Array.from(groups.keys())[0], ...config, skill: config.skill[0], tool: config.tool[0] };
if ( !label ) label = createRollLabel(config);
return config.passive ? createPassiveTag(label, config) : createRequestLink(createRollLink(label), config);
}
/* -------------------------------------------- */
+/**
+ * Create the buttons for a check requested in chat.
+ * @param {object} dataset
+ * @returns {object[]}
+ */
+function createCheckRequestButtons(dataset) {
+ const skills = dataset.skill?.split("|") ?? [];
+ const tools = dataset.tool?.split("|") ?? [];
+ if ( (skills.length + tools.length) <= 1 ) return [createRequestButton(dataset)];
+ const baseDataset = { ...dataset };
+ delete baseDataset.skill;
+ delete baseDataset.tool;
+ return [
+ ...skills.map(skill => createRequestButton({
+ ability: CONFIG.DND5E.skills[skill].ability, ...baseDataset, format: "short", skill, type: "skill"
+ })),
+ ...tools.map(tool => createRequestButton({
+ ability: CONFIG.DND5E.tools[tool]?.ability, ...baseDataset, format: "short", tool, type: "tool"
+ }))
+ ];
+}
+
+/* -------------------------------------------- */
+
/**
* Enrich a saving throw link.
* @param {object} config Configuration data.
@@ -413,7 +528,7 @@ async function enrichCheck(config, label, options) {
*
* @example Specify multiple abilities:
* ```[[/save ability=str/dex dc=20]]```
- * ```[[/save strength dexterity dc=20]]```
+ * ```[[/save strength dexterity 20]]```
* becomes
* ```html
*
@@ -1202,7 +1317,8 @@ async function rollAction(event) {
const MessageClass = getDocumentClass("ChatMessage");
let buttons;
- if ( dataset.type === "save" ) buttons = createSaveRequestButtons(dataset);
+ if ( dataset.type === "check" ) buttons = createCheckRequestButtons(dataset);
+ else if ( dataset.type === "save" ) buttons = createSaveRequestButtons(dataset);
else buttons = [createRequestButton({ ...dataset, format: "short" })];
const chatData = {