diff --git a/.eslintrc.yml b/.eslintrc.yml
index 7001ee9..4bb014b 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -47,6 +47,7 @@ globals:
ActiveEffect: readonly
Actor: readonly
CONFIG: readonly
+ CONST: readonly
ChatMessage: readonly
Combat: readonly
Combatant: readonly
diff --git a/lang/en.json b/lang/en.json
index 8058879..ba0d360 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -86,11 +86,11 @@
"DG.Skills.AddTypedOrCustomSkill": "Add Typed/Custom Skill",
"DG.Skills.AddSkill": "Add Skill",
"DG.Skills.Apply": "Apply",
- "DG.Skills.ApplySkillImprovements": "Apply Skill Improvements",
- "DG.Skills.ApplySkillImprovementsChatFlavor": "Improved the following skills by",
- "DG.Skills.ApplySkillImprovementsDialogLabel": "Improve skills by the following formula:",
- "DG.Skills.ApplySkillImprovementsDialogEffectsFollowing": "Effects the following skills:",
- "DG.Skills.ApplySkillImprovementsWarning": "No failed skills to improve.",
+ "DG.Skills.ApplySkillImprovements.Title": "Apply Skill Improvements",
+ "DG.Skills.ApplySkillImprovements.ChatFlavor": "Improved the following skills by {formula}:",
+ "DG.Skills.ApplySkillImprovements.DialogLabel": "Improve skills by the following formula: {formula}",
+ "DG.Skills.ApplySkillImprovements.DialogEffectsFollowing": "Effects the following skills: {skillList}",
+ "DG.Skills.ApplySkillImprovements.Warning": "No failed skills to improve.",
"DG.Skills.EditTypedOrCustomSkill": "Edit Typed or Custom Skill",
"DG.Skills.EditSkill": "Edit Skill",
"DG.Skills.SkillGroup": "Skill Group",
diff --git a/module/config.js b/module/config.js
index 9e06eae..5e10940 100644
--- a/module/config.js
+++ b/module/config.js
@@ -50,8 +50,20 @@ const DG = /** @type {const} */ ({
"ritual",
],
+ skillImprovementFormulas: {
+ // If choices are defined, the resulting setting will be a select menu
+ 1: "DG.Settings.improvementroll.1",
+ d3: "DG.Settings.improvementroll.2",
+ d4: "DG.Settings.improvementroll.3",
+ "d4-1": "DG.Settings.improvementroll.4",
+ },
+
// All the base rollable stats.
statistics: ["str", "con", "dex", "int", "pow", "cha"],
});
+// Set base template path for single source of truth for hbs locations.
+const BASE_TEMPLATE_PATH = /** @type {const} */ (`systems/${DG.ID}/templates`);
+
export default DG;
+export { BASE_TEMPLATE_PATH };
diff --git a/module/settings.js b/module/settings.js
index 60352ef..3860ba6 100644
--- a/module/settings.js
+++ b/module/settings.js
@@ -1,3 +1,5 @@
+import DG from "./config.js";
+
export default function registerSystemSettings() {
game.settings.register("deltagreen", "characterSheetStyle", {
name: game.i18n.localize("DG.Settings.charactersheet.name"),
@@ -45,14 +47,8 @@ export default function registerSystemSettings() {
scope: "world", // This specifies a world-stored setting
config: true, // This specifies that the setting appears in the configuration view
type: String,
- choices: {
- // If choices are defined, the resulting setting will be a select menu
- 1: game.i18n.localize("DG.Settings.improvementroll.1"),
- "1d3": game.i18n.localize("DG.Settings.improvementroll.2"),
- "1d4": game.i18n.localize("DG.Settings.improvementroll.3"),
- "1d4-1": game.i18n.localize("DG.Settings.improvementroll.4"),
- },
- default: "1d4", // The default value for the setting, per the most recent errata.
+ choices: DG.skillImprovementFormulas,
+ default: "d4", // The default value for the setting, per the most recent errata.
onChange: (value) => {
// A callback function which triggers when the setting is changed
// console.log(value)
@@ -70,7 +66,7 @@ export default function registerSystemSettings() {
requiresReload: true,
type: Boolean,
default: false,
- }
+ },
);
game.settings.register("deltagreen", "showImpossibleLandscapesContent", {
diff --git a/module/sheets/agent-sheet.js b/module/sheets/agent-sheet.js
index 4d713b7..a9f4aad 100644
--- a/module/sheets/agent-sheet.js
+++ b/module/sheets/agent-sheet.js
@@ -1,5 +1,8 @@
+import DG, { BASE_TEMPLATE_PATH } from "../config.js";
import DGActorSheet from "./base-actor-sheet.js";
+const { renderTemplate } = foundry.applications.handlebars;
+
/** @extends {DGActorSheet} */
export default class DGAgentSheet extends DGActorSheet {
/** @override */
@@ -9,7 +12,7 @@ export default class DGAgentSheet extends DGActorSheet {
clearBondDamage: DGAgentSheet._clearBondDamage,
resetBreakingPoint: DGAgentSheet._resetBreakingPoint,
// Other actions.
- applySkillImprovements: DGAgentSheet._applySkillImprovements,
+ applySkillImprovements: DGAgentSheet._processSkillImprovements,
},
});
@@ -84,75 +87,6 @@ export default class DGAgentSheet extends DGActorSheet {
/* -------------------------------------------- */
- static _applySkillImprovements(event, target) {
- const failedSkills = Object.entries(this.actor.system.skills).filter(
- (skill) => skill[1].failure,
- );
- const failedTypedSkills = Object.entries(
- this.actor.system.typedSkills,
- ).filter((skill) => skill[1].failure);
- if (failedSkills.length === 0 && failedTypedSkills.length === 0) {
- ui.notifications.warn("No Skills to Increase");
- return;
- }
-
- let htmlContent = "";
- let failedSkillNames = "";
- failedSkills.forEach(([skill], value) => {
- if (value === 0) {
- failedSkillNames += game.i18n.localize(`DG.Skills.${skill}`);
- } else {
- failedSkillNames += `, ${game.i18n.localize(`DG.Skills.${skill}`)}`;
- }
- });
- failedTypedSkills.forEach(([skillName, skillData], value) => {
- if (value === 0 && failedSkillNames === "") {
- failedSkillNames += `${game.i18n.localize(
- `DG.TypeSkills.${skillData.group.split(" ").join("")}`,
- )} (${skillData.label})`;
- } else {
- failedSkillNames += `, ${game.i18n.localize(
- `DG.TypeSkills.${skillData.group.split(" ").join("")}`,
- )} (${skillData.label})`;
- }
- });
-
- const baseRollFormula = game.settings.get(
- "deltagreen",
- "skillImprovementFormula",
- );
-
- htmlContent += `
`;
- htmlContent += ` `;
- htmlContent += `
`;
- htmlContent += ` ${game.i18n.localize(
- "DG.Skills.ApplySkillImprovementsDialogEffectsFollowing",
- )} ${failedSkillNames} `;
- htmlContent += ``;
-
- new Dialog({
- content: htmlContent,
- title:
- game.i18n.translations.DG?.Skills?.ApplySkillImprovements ??
- "Apply Skill Improvements",
- default: "add",
- buttons: {
- apply: {
- label: game.i18n.translations.DG?.Skills?.Apply ?? "Apply",
- callback: (btn) => {
- this._applySkillImprovements(
- baseRollFormula,
- failedSkills,
- failedTypedSkills,
- );
- },
- },
- },
- }).render(true);
- }
-
/** Resets the actor's current breaking point based on their sanity and POW statistics. */
static _resetBreakingPoint() {
const systemData = this.actor.system;
@@ -168,115 +102,217 @@ export default class DGAgentSheet extends DGActorSheet {
_resetBreakingPoint(event) {
event.preventDefault();
- let currentBreakingPoint = 0;
+ const currentBreakingPoint = Math.max(
+ this.actor.system.sanity.value - this.actor.system.statistics.pow.value,
+ 0,
+ );
- currentBreakingPoint =
- this.actor.system.sanity.value - this.actor.system.statistics.pow.value;
-
- if (currentBreakingPoint < 0) {
- currentBreakingPoint = 0;
- }
-
- const updatedData = foundry.utils.duplicate(this.actor.system);
-
- updatedData.sanity.currentBreakingPoint = currentBreakingPoint;
-
- this.actor.update({ system: updatedData });
+ this.actor.update({
+ "system.sanity.currentBreakingPoint": currentBreakingPoint,
+ });
}
- // For any skills a user has checked off as failed, roll the improvement and update the agent's skills to their new values
- async _applySkillImprovements(
- baseRollFormula,
+ /**
+ * Runs through the whole process of improving skills,
+ * i.e., prompting the user, rolling, and creating the chat card.
+ *
+ * @returns {Promise}
+ */
+ static async _processSkillImprovements() {
+ const { skills, typedSkills } = this.actor.system;
+
+ const failedSkills = Object.values(skills).filter((skill) => skill.failure);
+ const failedTypedSkills = Object.values(typedSkills).filter(
+ (skill) => skill.failure,
+ );
+
+ if (failedSkills.length + failedTypedSkills.length === 0) {
+ ui.notifications.warn("DG.Skills.ApplySkillImprovements.Warning", {
+ localize: true,
+ });
+ return null;
+ }
+
+ const baseRollFormula = game.settings.get(DG.ID, "skillImprovementFormula");
+
+ const prompt = await DGAgentSheet._createSkillImprovementDialog(
+ baseRollFormula,
+ failedSkills,
+ failedTypedSkills,
+ );
+
+ if (!prompt) return null;
+
+ const { roll, resultObj } = await this._createSkillImprovementRolls(
+ baseRollFormula,
+ failedSkills,
+ failedTypedSkills,
+ );
+
+ await this._createSkillImprovementChatCard(
+ failedSkills,
+ failedTypedSkills,
+ roll,
+ resultObj,
+ );
+
+ return this._applySkillImprovements(
+ failedSkills,
+ failedTypedSkills,
+ resultObj,
+ );
+ }
+
+ /**
+ * A map of skill keys -> number to improve them by
+ * @typedef {Object} ResultObj
+ */
+
+ /**
+ * @typedef {Object} FailedSkill
+ */
+
+ /**
+ * @typedef {FailedSkill} FailedTypedSkill
+ */
+
+ /**
+ * The formula used to calculate skill improvements
+ * Note. There is not a leading number of dice here, just 1 or dX-Y.
+ * @typedef {"1"|"d3"|"d4"|"d4-1"} SkillImprovementFormula
+ */
+
+ /**
+ * Creates and displays a dialog to approve applying skill improvements to failed skills.
+ *
+ * @param {SkillImprovementFormula} baseFormula - The formula used to calculate skill improvements.
+ * @param {FailedSkill[]} failedSkills - An array of failed skills.
+ * @param {FailedTypedSkill[]} failedTypedSkills - An array of failed typed skills.
+ *
+ * @returns {Promise} - A promise that resolves to `true` if accepted, `false` otherwise.
+ */
+ static async _createSkillImprovementDialog(
+ baseFormula,
failedSkills,
failedTypedSkills,
) {
- const actorData = this.actor.system;
- const resultList = [];
- let rollFormula;
+ const localizedFailedSkills = failedSkills.map((skill) =>
+ game.i18n.localize(`DG.Skills.${skill.key}`),
+ );
- // Define the amount of dice being rolled, if any.
- switch (baseRollFormula) {
- case "1":
- rollFormula = 1;
- break;
- case "1d3":
- rollFormula = `${failedSkills.length + failedTypedSkills.length}d3`;
- break;
- case "1d4":
- case "1d4-1":
- rollFormula = `${failedSkills.length + failedTypedSkills.length}d4`;
- break;
- default:
+ const localizedFailedTypedSkills = failedTypedSkills.map((skill) => {
+ const groupKey = `DG.TypeSkills.${skill.group.replace(/\s+/g, "")}`;
+ const groupLabel = game.i18n.localize(groupKey);
+ return `${groupLabel} (${skill.label})`;
+ });
+
+ const failedSkillNames = [
+ ...localizedFailedSkills,
+ ...localizedFailedTypedSkills,
+ ].join(", ");
+
+ const content = await renderTemplate(
+ `${BASE_TEMPLATE_PATH}/dialog/apply-skill-improvements.html`,
+ {
+ failedSkillNames,
+ baseFormula: `${baseFormula === "1" ? "1" : `1${baseFormula}`}%`,
+ },
+ );
+
+ return Dialog.wait({
+ content,
+ title: game.i18n.localize("DG.Skills.ApplySkillImprovements.Title"),
+ default: "apply",
+ buttons: {
+ apply: {
+ label: game.i18n.localize("DG.Skills.Apply"),
+ icon: "",
+ },
+ },
+ });
+ }
+
+ /**
+ * Generates and evaluates the rolls for skill improvements based on failed skills.
+ *
+ * @param {SkillImprovementFormula} baseFormula - The formula used to calculate skill improvements.
+ * @param {FailedSkill[]} failedSkills - An array of failed skills.
+ * @param {FailedTypedSkill[]} failedTypedSkills - An array of failed typed skills.
+ *
+ * @returns {Promise<{roll: Roll|undefined, resultObj: ResultObj}>} - An object containing the roll result and a map of skill keys to improvement values.
+ *
+ * @throws {Error} - Throws an error if the baseFormula is unknown.
+ */
+ async _createSkillImprovementRolls(
+ baseFormula,
+ failedSkills,
+ failedTypedSkills,
+ ) {
+ const totalFailures = failedSkills.length + failedTypedSkills.length;
+
+ if (!Object.keys(DG.skillImprovementFormulas).includes(baseFormula)) {
+ throw new Error(`Unknown roll formula: ${baseFormula}`);
}
+ const rollFormula =
+ baseFormula === "1" ? "1" : `${totalFailures}${baseFormula}`;
+
let roll;
- if (rollFormula !== 1) {
- roll = new Roll(rollFormula, actorData);
- await roll.evaluate();
- // Put the results into a list.
- roll.terms[0].results.forEach((result) =>
- resultList.push(
- baseRollFormula === "1d4-1" ? result.result - 1 : result.result,
- ),
- );
+ const resultObj = {};
+ if (rollFormula !== "1") {
+ roll = await new Roll(rollFormula, this.actor.system).evaluate();
+ [...failedSkills, ...failedTypedSkills].forEach((skill, index) => {
+ const { result } = roll.terms[0].results[index];
+ resultObj[skill.key] = result;
+ });
}
- // This will be end up being a list of skills and how much each were improved by. It gets modified in the following loops.
- let improvedSkillList = "";
+ return { roll, resultObj };
+ }
- // Get copy of current system data, will update this and then apply all changes at once synchronously at the end.
- const updatedData = foundry.utils.duplicate(actorData);
+ /**
+ * Create a chat card to record skill improvements.
+ *
+ * @param {FailedSkill[]} failedSkills - array of failed skills
+ * @param {FailedTypedSkill[]} failedTypedSkills - array of failed typed skills
+ * @param {Roll|undefined} roll - the improvement roll
+ * @param {ResultObj} resultObj
+ *
+ * @returns {Promise}
+ */
+ _createSkillImprovementChatCard(
+ failedSkills,
+ failedTypedSkills,
+ roll,
+ resultObj,
+ ) {
+ const localizeFailedSkills = (skillsArray) => {
+ return skillsArray.map((skill) => {
+ const increment = resultObj[skill.key] ?? 1;
+ const label =
+ skill.label ?? game.i18n.localize(`DG.Skills.${skill.key}`); // fallback for regular skills
+ const groupLabel = skill.group
+ ? `${game.i18n.localize(
+ `DG.TypeSkills.${skill.group.replace(/\s+/g, "")}`,
+ )} (${label})`
+ : label;
- failedSkills.forEach(([skill], value) => {
- updatedData.skills[skill].proficiency += resultList[value] ?? 1; // Increase proficiency by die result or by 1 if there is no dice roll.
- updatedData.skills[skill].failure = false;
+ return `${groupLabel}: +${increment}%`;
+ });
+ };
- // So we can record the regular skills improved and how much they were increased by in chat.
- // The if statement tells us whether to add a comma before the term or not.
- if (value === 0) {
- improvedSkillList += `${game.i18n.localize(
- `DG.Skills.${skill}`,
- )}: +${resultList[value] ?? 1}%`;
- } else {
- improvedSkillList += `, ${game.i18n.localize(
- `DG.Skills.${skill}`,
- )}: +${resultList[value] ?? 1}%`;
- }
- });
+ const failedSkillNames = localizeFailedSkills(failedSkills);
+ const failedTypedSkillNames = localizeFailedSkills(failedTypedSkills);
- failedTypedSkills.forEach(([skillName, skillData], value) => {
- // We must increase value in the following line by the length of failedSkills, so that we index the entire resultList.
- // Otherwise we would be adding the same die results to regular skills and typed skills.
- updatedData.typedSkills[skillName].proficiency +=
- resultList[value + failedSkills.length] ?? 1;
- updatedData.typedSkills[skillName].failure = false;
-
- // So we can record the typed skills improved and how much they were increased by in chat.
- // The if statement tells us whether to add a comma before the term or not.
- if (value === 0 && improvedSkillList === "") {
- improvedSkillList += `${game.i18n.localize(
- `DG.TypeSkills.${skillData.group.split(" ").join("")}`,
- )} (${skillData.label}): +${
- resultList[value + failedSkills.length] ?? 1
- }%`;
- } else {
- improvedSkillList += `, ${game.i18n.localize(
- `DG.TypeSkills.${skillData.group.split(" ").join("")}`,
- )} (${skillData.label}): +${
- resultList[value + failedSkills.length] ?? 1
- }%`;
- }
- });
-
- // Probably not worth triggering the update if the user didn't pick any skills
- if (improvedSkillList !== "") {
- await this.actor.update({ system: updatedData });
- }
-
- let html;
- html = ``;
- html += `
${improvedSkillList}
`;
- html += `
`;
+ // Prepare chat data
+ const content = [...failedSkillNames, ...failedTypedSkillNames].join(", ");
+ const flavor = game.i18n.format(
+ "DG.Skills.ApplySkillImprovements.ChatFlavor",
+ { formula: `${roll.formula?.replace(/^.*d/, "1d") ?? "1"}%` }, // formula = 1, otherwise 1dX-Y (i.e. 1d3, 1d4, 1d4-1)
+ );
+ const type = roll
+ ? CONST.CHAT_MESSAGE_TYPES.ROLL
+ : CONST.CHAT_MESSAGE_TYPES.OTHER;
const chatData = {
speaker: ChatMessage.getSpeaker({
@@ -284,19 +320,45 @@ export default class DGAgentSheet extends DGActorSheet {
token: this.token,
alias: this.actor.name,
}),
- content: html,
- flavor: `${game.i18n.localize(
- "DG.Skills.ApplySkillImprovementsChatFlavor",
- )} +${baseRollFormula}%:`,
- type: baseRollFormula === "1" ? 0 : 5, // 0 = CHAT_MESSAGE_TYPES.OTHER, 5 = CHAT_MESSAGE_TYPES.ROLL
- rolls: baseRollFormula === "1" ? [] : [roll], // If adding flat +1, there is no roll.
+ content,
+ flavor,
+ type,
+ rolls: roll ? [roll] : [],
rollMode: game.settings.get("core", "rollMode"),
};
- // Create a message from this roll, if there is one.
if (roll) return roll.toMessage(chatData);
+ return ChatMessage.create(chatData);
+ }
- // If no roll, create a chat message directly.
- return ChatMessage.create(chatData, {});
+ /**
+ * Updates the actor's skills / typed skills with the improvements,
+ * persisting the changes to the database.
+ *
+ * @param {FailedSkill[]} failedSkills - array of failed skills that need to be updated
+ * @param {FailedTypedSkill[]} failedTypedSkills - array of failed skills that need to be updated
+ * @param {ResultObj} resultObj
+ * @returns {Promise} - the update promise
+ */
+ _applySkillImprovements(failedSkills, failedTypedSkills, resultObj) {
+ const updateSkills = (skillsArray, updatedTarget) => {
+ skillsArray.forEach((skill) => {
+ const increment = resultObj[skill.key] ?? 1;
+ updatedTarget[skill.key].proficiency += increment;
+ updatedTarget[skill.key].failure = false;
+ });
+ };
+
+ const actorData = this.actor.system;
+ // Get data and update it.
+ const updatedSkills = foundry.utils.duplicate(actorData.skills);
+ const updatedTypedSkills = foundry.utils.duplicate(actorData.typedSkills);
+ updateSkills(failedSkills, updatedSkills);
+ updateSkills(failedTypedSkills, updatedTypedSkills);
+
+ // Send updates to database.
+ return this.actor.update({
+ system: { skills: updatedSkills, typedSkills: updatedTypedSkills },
+ });
}
}
diff --git a/module/sheets/base-sheet.js b/module/sheets/base-sheet.js
index de42d33..f4de812 100644
--- a/module/sheets/base-sheet.js
+++ b/module/sheets/base-sheet.js
@@ -1,4 +1,4 @@
-import DG from "../config.js";
+import DG, { BASE_TEMPLATE_PATH } from "../config.js";
const HbsAppMixin = foundry.applications.api.HandlebarsApplicationMixin;
@@ -11,8 +11,7 @@ const DGSheetMixin = (Base) => {
form: { submitOnChange: true },
});
- /** @inheritdoc */
- static TEMPLATE_PATH = /** @type {const} */ (`systems/${DG.ID}/templates`);
+ static TEMPLATE_PATH = BASE_TEMPLATE_PATH;
static get THEME() {
return game.settings.get(DG.ID, "characterSheetStyle");
diff --git a/templates/actor/partials/custom-skills-partial.html b/templates/actor/partials/custom-skills-partial.html
index 4d01753..fd0df26 100644
--- a/templates/actor/partials/custom-skills-partial.html
+++ b/templates/actor/partials/custom-skills-partial.html
@@ -14,7 +14,7 @@
{{#if_eq actor.type "agent" }}
- {{localize 'DG.Skills.ApplySkillImprovements'}}
+ {{localize 'DG.Skills.ApplySkillImprovements.Title'}}
{{/if_eq}}
diff --git a/templates/dialog/apply-skill-improvements.html b/templates/dialog/apply-skill-improvements.html
new file mode 100644
index 0000000..213281f
--- /dev/null
+++ b/templates/dialog/apply-skill-improvements.html
@@ -0,0 +1,7 @@
+
+
+
+
+ {{{localize "DG.Skills.ApplySkillImprovements.DialogEffectsFollowing" skillList=failedSkillNames}}}
+
+