From 335b90fbc469a745df8b22c4b12bfc0937ee1bbb Mon Sep 17 00:00:00 2001
From: Ian Bennett <34078802+ian612@users.noreply.github.com>
Date: Thu, 26 Dec 2024 13:00:47 -0500
Subject: [PATCH 1/3] Add bugfix code for inline checks
---
src/init.mjs | 470 +++++++++++++++++-
.../bugfix-code/system/enrichers/base.js | 233 +++++++++
.../bugfix-code/system/enrichers/check.js | 141 ++++++
src/module/test.js | 5 +
src/module/utils/skill-names.js | 100 ++++
5 files changed, 945 insertions(+), 4 deletions(-)
create mode 100644 src/module/bugfix-code/system/enrichers/base.js
create mode 100644 src/module/bugfix-code/system/enrichers/check.js
create mode 100644 src/module/test.js
create mode 100644 src/module/utils/skill-names.js
diff --git a/src/init.mjs b/src/init.mjs
index 0f1523b..3603de2 100644
--- a/src/init.mjs
+++ b/src/init.mjs
@@ -1,4 +1,5 @@
import { preloadHandlebarsTemplates } from './module/templates.js';
+// import { CheckEnricher } from './module/bugfix-code/system/enrichers/check.js';
const MODULE_ID = 'sfrpg-item-sheets';
const itemSizeArmorClassModifier = {
@@ -13,6 +14,17 @@ const itemSizeArmorClassModifier = {
"colossal": 8
};
+Hooks.once("init", () => {
+ preloadHandlebarsTemplates()
+ Items.registerSheet(MODULE_ID, EnhancedItemSheetMixin(game.sfrpg.applications.ItemSheetSFRPG), {makeDefault: true})
+ const test = new CheckEnricher();
+ console.log("------------------------------------------------------------------------------------------------------------");
+ console.log(CONFIG);
+ console.log(test);
+ CONFIG.TextEditor.enrichers[2] = new CheckEnricher();
+ // CONFIG.TextEditor.enrichers.push(new BrowserEnricher(), new IconEnricher(), new CheckEnricher(), new TemplateEnricher());
+})
+
function EnhancedItemSheetMixin(SheetClass) {
const RollContext = game.sfrpg.rolls.RollContext;
return class EnhancedItemSheetSFRPG extends SheetClass {
@@ -421,7 +433,457 @@ function EnhancedItemSheetMixin(SheetClass) {
}
}
-Hooks.once("init", () => {
- preloadHandlebarsTemplates()
- Items.registerSheet(MODULE_ID, EnhancedItemSheetMixin(game.sfrpg.applications.ItemSheetSFRPG), {makeDefault: true})
-})
\ No newline at end of file
+class CheckNameHelper {
+ /**
+ * Take the full name for a check, and return the 3-letter identifier
+ * @param {String} fullName The full name for the check, such as "life-science" or "acrobatics"
+ * @returns {String} The 3-letter name, if it exists, otherwise the inputted full name
+ */
+ static shortFormName(fullName) {
+ return {
+ "acrobatics": "acr",
+ "athletics": "ath",
+ "bluff": "blu",
+ "computers": "com",
+ "culture": "cul",
+ "diplomacy":"dip",
+ "disguise": "dis",
+ "engineering":"eng",
+ "intimidate": "int",
+ "life-science":"lsc",
+ "medicine": "med",
+ "mysticism": "mys",
+ "perception": "per",
+ "profession": "pro",
+ "physical-science":"phs",
+ "piloting": "pil",
+ "sense-motive":"sen",
+ "sleight-of-hand":"sle",
+ "stealth": "ste",
+ "survival": "sur",
+
+ "fortitude": "fort",
+ "reflex": "reflex",
+ "will": "will",
+
+ "strength": "str",
+ "dexterity": "dex",
+ "constitution": "con",
+ "intelligence": "int",
+ "wisdom": "wis",
+ "charisma": "cha",
+
+ "caster-level": "caster-level"
+ }[fullName] || fullName;
+ }
+
+ /**
+ * Take the 3-letter identifier for a check, and return the full name
+ * @param {String} threeLetter The 3-letter identifier for the check, such as "lsc" or "acr"
+ * @returns {String} The full name, if it exists, otherwise the inputted 3-letter name
+ */
+ static longFormName(threeLetter) {
+ return {
+ "acr": "acrobatics",
+ "ath": "athletics",
+ "blu": "bluff",
+ "com":"computers",
+ "cul": "culture",
+ "dip": "diplomacy",
+ "dis": "disguise",
+ "eng": "engineering",
+ "int": "intimidate",
+ "lsc": "life-science",
+ "med": "medicine",
+ "mys": "mysticism",
+ "per": "perception",
+ "pro": "profession",
+ "phs": "physical-science",
+ "pil": "piloting",
+ "sen": "sense-motive",
+ "sle": "sleight-of-hand",
+ "ste": "stealth",
+ "sur": "survival",
+
+ "fort": "fortitude",
+ "reflex": "reflex",
+ "will": "will",
+
+ "caster-level": "caster-level"
+ }[threeLetter] || threeLetter;
+ }
+
+ /**
+ * Same as longformName, but a seperate function to stop namespace collision between the "int" of intimidate and intelligence
+ * @see longFormName
+ */
+ static longFormNameAbilities(threeLetter) {
+ return {
+ "str": "strength",
+ "dex": "dexterity",
+ "con": "constitution",
+ "int": "intelligence",
+ "wis": "wisdom",
+ "cha": "charisma"
+ }[threeLetter] || threeLetter;
+ }
+}
+
+class BaseEnricher {
+
+ /** @type {CustomEnricher} */
+ constructor() {
+ if (this.constructor === BaseEnricher) throw new Error(
+ "The BaseEnricher class is an abstract class and may not be instantiated."
+ );
+ this.pattern = this.regex;
+ this.enricher = this.enricherFunc.bind(this);
+ }
+
+ /** --------
+ | |
+ | Getters |
+ | |
+ ----------*/
+
+ /**
+ * The RegExp to capture the text.
+ * @returns {RegExp}
+ */
+ get regex() {
+ return new RegExp(`(@${this.enricherType})(\\[[^\\]]+)](?:{([^}]+)})?`, "gm");
+ }
+
+ /**
+ * The type of custom enricher, i.e the word following the @
+ * @returns {String}
+ */
+ get enricherType() {
+ throw new Error("This method must be implemented on subclasses of BaseEnricher.");
+ }
+
+ /**
+ * Valid options for the type argument
+ * @returns {String[]}
+ */
+ get validTypes() {
+ throw new Error("This method must be implemented on subclasses of BaseEnricher.");
+ }
+
+ /**
+ * An object of FA icons to be used in the element
+ * @returns {Object}
+ */
+ get icons() {
+ throw new Error("This method must be implemented on subclasses of BaseEnricher.");
+ }
+
+ /** -------------------
+ | |
+ | Element Generation |
+ | |
+ ----------------------*/
+
+ /**
+ * Transform the Regex match array into an enriched element, performing validation.
+ * @callback EnricherFunction
+ * @param {RegExp} match A Regex match array from the inputted text
+ * @param {Object} options
+ * @returns {HTMLElement} The enriched element
+ */
+ enricherFunc(match, options) {
+ this.match = match;
+
+ if (this.match[3]) this.name = this.match[3];
+ else this.name = undefined;
+
+ this.parseArgs();
+
+ // Early return an error element if invalid
+ if (!this.isValid()) return this.element;
+
+ this.validateName();
+
+ this.element = this.createElement();
+
+ return this.element;
+ }
+
+ /**
+ * Transform the args in the orginal text to an object
+ */
+ parseArgs() {
+ // Split each argument from the square brackets
+ const args = this.match[2].split("|");
+
+ this.args = args.reduce((obj, i) => {
+ // Split each arg into a key and a value
+ // Matches a colon with a letter before, and either a JSON or character after.
+ // Set up as to not split colons in JSONs
+ const split = i.match(/(\w*):({.*}?|.+)/);
+ if (split?.length > 0) obj[split[1]] = split[2];
+
+ return obj;
+ }, {});
+ }
+
+ /**
+ * Checks if there is a type argument, and that it is valid for the enricher's type.
+ * Sets this.element if invalid for an early return.
+ * @returns {Boolean}
+ */
+ isValid() {
+ if (!this.args.type || !this.validTypes.includes(this.args.type)) {
+ return this._failValidation("Type");
+ }
+
+ return true;
+ }
+
+ /**
+ * Create an error element after isValid() fails
+ * @param {String} failedArg The argument that failed validation, to be used in the error element
+ * @returns {false}
+ */
+ _failValidation(failedArg) {
+ const strong = document.createElement("strong");
+ strong.innerText = `${this.enricherType} parsing failed! ${failedArg} is invalid.`;
+ this.element = strong;
+ return false;
+ }
+
+ /**
+ * Sets a default name if none was given
+ */
+ validateName() {
+ this.name ||= `${this.args.type.capitalize()} ${this.enricherType}`;
+ }
+
+ /**
+ * Create a HTML element and affix some data.
+ * Can be called in subclasses by assigning the super to a local variable.
+ * @returns {HTMLAnchorElement}
+ */
+ createElement() {
+ let a = document.createElement("a");
+
+ a.dataset.action = this.enricherType;
+ a.dataset.type = this.args.type;
+
+ a.classList.add("enriched-link");
+ a.draggable = false;
+
+ a.innerText = this.name;
+
+ if (this.#_hasRepost) a = this.addRepost(a);
+
+ return a;
+ }
+
+ /** -------
+ | |
+ | Repost |
+ | |
+ -----------*/
+
+ /**
+ * Should this enricher have a repost button appended to created elements?
+ * Create both a publicly accessible static variable and an internal instance one.
+ * @type {Boolean}
+ */
+ static hasRepost = false;
+ /** @type {Boolean} */
+ #_hasRepost = this.constructor.hasRepost;
+
+ /**
+ * Take an anchor element and append a repost button
+ * @param {HTMLAnchorElement} a The original anchor
+ * @returns The inputted Anchor, with a repost button appended
+ */
+ addRepost(a) {
+ const repost = document.createElement("i");
+ repost.classList.add("fas", "fa-comment-alt", "repost");
+ repost.dataset.tooltip = "SFRPG.Enrichers.SendToChat";
+
+ a.append(repost);
+
+ return a;
+ }
+
+ /**
+ * Handle repost button click, sending a chat message of the current target to chat.
+ * @param {Event} event
+ * @returns Create a chat message
+ */
+ static repostListener(event) {
+ event.stopPropagation();
+
+ return ChatMessage.create({content: event.currentTarget.parentElement.outerHTML});
+ }
+
+ /** ---------
+ | |
+ | Listener |
+ | |
+ ------------*/
+
+ /**
+ * Whether the enricher has an event listener.
+ * @type {Boolean}
+ */
+ static hasListener = false;
+
+ /**
+ * A callback function to run when the element is clicked.
+ * @param {Event} event The DOM event that triggers the listener
+ * @returns {void}
+ */
+ static listener(event) {}
+
+ /**
+ * Add Event listeners to the DOM body at startup.
+ */
+ static addListeners() {
+ const body = $("body");
+ body.on("click", `i.repost`, this.repostListener);
+ for (const [action, cls] of Object.entries(CONFIG.SFRPG.enricherTypes)) {
+ if (cls.hasListener) body.on("click", `a[data-action="${action}"]`, cls.listener);
+ }
+ }
+}
+
+class CheckEnricher extends BaseEnricher {
+ // @Check[type:athletics]
+ // @Check[type:life-science]
+ // @Check[type:reflex]
+ constructor() {
+ super();
+ }
+
+ /** @inheritdoc */
+ get enricherType() {
+ return "Check";
+ }
+
+ /** @inheritdoc */
+ get validTypes() {
+ return [
+ ...Object.keys(CONFIG.SFRPG.skills),
+ ...Object.keys(CONFIG.SFRPG.saves),
+ ...Object.keys(CONFIG.SFRPG.abilities),
+ "caster-level"
+ ];
+ }
+
+ /** @inheritdoc */
+ get icons() {
+ return {
+ "acrobatics": "fa-person-walking",
+ "athletics": "fa-dumbbell",
+ "bluff": "fa-comment",
+ "computers": "fa-computer",
+ "culture": "fa-flag",
+ "diplomacy": "fa-handshake",
+ "disguise": "fa-mask",
+ "engineering": "fa-gear",
+ "intimidate": "fa-face-angry",
+ "life-science": "fa-dna",
+ "medicine": "fa-syringe",
+ "mysticism": "fa-hand-sparkles",
+ "perception": "fa-magnifying-glass",
+ "profession": "fa-user-tie",
+ "physical-science": "fa-flask",
+ "piloting": "fa-plane",
+ "sense-motive": "fa-person-circle-question",
+ "sleight-of-hand": "fa-hands",
+ "stealth": "fa-moon",
+ "survival": "fa-campground",
+
+ "fortitude": "fa-shield-heart",
+ "reflex": "fa-person-running",
+ "will": "fa-brain",
+
+ "strength": "fa-weight-hanging",
+ "dexterity": "fa-feather-pointed",
+ "constitution": "fa-heart-pulse",
+ "intelligence": "fa-glasses",
+ "wisdom": "fa-mountain-sun",
+ "charisma": "fa-people-arrows",
+
+ "caster-level": "fa-wand-magic-sparkles"
+ };
+ }
+
+ get checkType() {
+ const shortName = CheckNameHelper.shortFormName(this.args.type);
+ const C = CONFIG.SFRPG;
+
+ if (shortName in C.skills) return "skill";
+ else if (shortName in C.saves) return "save";
+ else if (shortName in C.abilities) return "ability";
+ else return null;
+ }
+
+ get localizedType() {
+ const C = CONFIG.SFRPG;
+ const type = CheckNameHelper.shortFormName(this.args.type);
+
+ switch (this.checkType) {
+ case "skill": return C.skills[type];
+ case "save": return C.saves[type];
+ case "ability": return C.abilities[type];
+ default: return "";
+ }
+ }
+
+ /**
+ * @override to check using the 3-letter identifier for the type against the valid types (which are 3 letter identifiers).
+ * Inputted types are full names.
+ */
+ isValid() {
+ if (!this.args.type || !this.validTypes.includes(CheckNameHelper.shortFormName(this.args.type))) {
+ return this._failValidation("Type");
+ }
+
+ return true;
+ }
+
+ validateName() {
+ const i18nPath = this.checkType === "save" ? "SFRPG.Save" : "SFRPG.Check";
+ const localizedCheck = game.i18n.localize(i18nPath);
+
+ this.name ||= `${this.localizedType} ${localizedCheck}`;
+ }
+
+ /**
+ * @extends BaseEnricher
+ * @returns {HTMLAnchorElement} */
+ createElement() {
+ const a = super.createElement();
+
+ if (this.args.dc) a.dataset.dc = parseInt(this.args.dc);
+ const iconSlug = (this.checkType === "ability") ? CheckNameHelper.longFormNameAbilities(this.args.type) : CheckNameHelper.longFormName(this.args.type);
+
+ a.innerHTML = `${a.innerHTML}`;
+
+ return a;
+
+ }
+
+ static hasRepost = true;
+ static hasListener = true;
+
+ static listener(event) {
+ const data = event.currentTarget.dataset;
+
+ const actor = _token?.actor ?? game.user?.character;
+ if (!actor) return ui.notifications.error("You must have a token or an actor selected.");
+ const id = CheckNameHelper.shortFormName(data.type);
+
+ if (id in CONFIG.SFRPG.skills) actor.rollSkill(id, { event });
+ else if (id in CONFIG.SFRPG.saves) actor.rollSave(id, { event });
+ else if (id in CONFIG.SFRPG.abilities) actor.rollAbility(id, { event });
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/module/bugfix-code/system/enrichers/base.js b/src/module/bugfix-code/system/enrichers/base.js
new file mode 100644
index 0000000..eebd349
--- /dev/null
+++ b/src/module/bugfix-code/system/enrichers/base.js
@@ -0,0 +1,233 @@
+/**
+ * @typedef {Object} CustomEnricher
+ * @property {RegExp} pattern
+ * @property {EnricherFunction} enricher
+ */
+
+/**
+ * Abstract base class for enrichers which carries validation and basic element creation.
+ * @abstract
+ * @class
+ */
+export class BaseEnricher {
+
+ /** @type {CustomEnricher} */
+ constructor() {
+ if (this.constructor === BaseEnricher) throw new Error(
+ "The BaseEnricher class is an abstract class and may not be instantiated."
+ );
+ this.pattern = this.regex;
+ this.enricher = this.enricherFunc.bind(this);
+ }
+
+ /** --------
+ | |
+ | Getters |
+ | |
+ ----------*/
+
+ /**
+ * The RegExp to capture the text.
+ * @returns {RegExp}
+ */
+ get regex() {
+ return new RegExp(`(@${this.enricherType})(\\[[^\\]]+)](?:{([^}]+)})?`, "gm");
+ }
+
+ /**
+ * The type of custom enricher, i.e the word following the @
+ * @returns {String}
+ */
+ get enricherType() {
+ throw new Error("This method must be implemented on subclasses of BaseEnricher.");
+ }
+
+ /**
+ * Valid options for the type argument
+ * @returns {String[]}
+ */
+ get validTypes() {
+ throw new Error("This method must be implemented on subclasses of BaseEnricher.");
+ }
+
+ /**
+ * An object of FA icons to be used in the element
+ * @returns {Object}
+ */
+ get icons() {
+ throw new Error("This method must be implemented on subclasses of BaseEnricher.");
+ }
+
+ /** -------------------
+ | |
+ | Element Generation |
+ | |
+ ----------------------*/
+
+ /**
+ * Transform the Regex match array into an enriched element, performing validation.
+ * @callback EnricherFunction
+ * @param {RegExp} match A Regex match array from the inputted text
+ * @param {Object} options
+ * @returns {HTMLElement} The enriched element
+ */
+ enricherFunc(match, options) {
+ this.match = match;
+
+ if (this.match[3]) this.name = this.match[3];
+ else this.name = undefined;
+
+ this.parseArgs();
+
+ // Early return an error element if invalid
+ if (!this.isValid()) return this.element;
+
+ this.validateName();
+
+ this.element = this.createElement();
+
+ return this.element;
+ }
+
+ /**
+ * Transform the args in the orginal text to an object
+ */
+ parseArgs() {
+ // Split each argument from the square brackets
+ const args = this.match[2].split("|");
+
+ this.args = args.reduce((obj, i) => {
+ // Split each arg into a key and a value
+ // Matches a colon with a letter before, and either a JSON or character after.
+ // Set up as to not split colons in JSONs
+ const split = i.match(/(\w*):({.*}?|.+)/);
+ if (split?.length > 0) obj[split[1]] = split[2];
+
+ return obj;
+ }, {});
+ }
+
+ /**
+ * Checks if there is a type argument, and that it is valid for the enricher's type.
+ * Sets this.element if invalid for an early return.
+ * @returns {Boolean}
+ */
+ isValid() {
+ if (!this.args.type || !this.validTypes.includes(this.args.type)) {
+ return this._failValidation("Type");
+ }
+
+ return true;
+ }
+
+ /**
+ * Create an error element after isValid() fails
+ * @param {String} failedArg The argument that failed validation, to be used in the error element
+ * @returns {false}
+ */
+ _failValidation(failedArg) {
+ const strong = document.createElement("strong");
+ strong.innerText = `${this.enricherType} parsing failed! ${failedArg} is invalid.`;
+ this.element = strong;
+ return false;
+ }
+
+ /**
+ * Sets a default name if none was given
+ */
+ validateName() {
+ this.name ||= `${this.args.type.capitalize()} ${this.enricherType}`;
+ }
+
+ /**
+ * Create a HTML element and affix some data.
+ * Can be called in subclasses by assigning the super to a local variable.
+ * @returns {HTMLAnchorElement}
+ */
+ createElement() {
+ let a = document.createElement("a");
+
+ a.dataset.action = this.enricherType;
+ a.dataset.type = this.args.type;
+
+ a.classList.add("enriched-link");
+ a.draggable = false;
+
+ a.innerText = this.name;
+
+ if (this.#_hasRepost) a = this.addRepost(a);
+
+ return a;
+ }
+
+ /** -------
+ | |
+ | Repost |
+ | |
+ -----------*/
+
+ /**
+ * Should this enricher have a repost button appended to created elements?
+ * Create both a publicly accessible static variable and an internal instance one.
+ * @type {Boolean}
+ */
+ static hasRepost = false;
+ /** @type {Boolean} */
+ #_hasRepost = this.constructor.hasRepost;
+
+ /**
+ * Take an anchor element and append a repost button
+ * @param {HTMLAnchorElement} a The original anchor
+ * @returns The inputted Anchor, with a repost button appended
+ */
+ addRepost(a) {
+ const repost = document.createElement("i");
+ repost.classList.add("fas", "fa-comment-alt", "repost");
+ repost.dataset.tooltip = "SFRPG.Enrichers.SendToChat";
+
+ a.append(repost);
+
+ return a;
+ }
+
+ /**
+ * Handle repost button click, sending a chat message of the current target to chat.
+ * @param {Event} event
+ * @returns Create a chat message
+ */
+ static repostListener(event) {
+ event.stopPropagation();
+
+ return ChatMessage.create({content: event.currentTarget.parentElement.outerHTML});
+ }
+
+ /** ---------
+ | |
+ | Listener |
+ | |
+ ------------*/
+
+ /**
+ * Whether the enricher has an event listener.
+ * @type {Boolean}
+ */
+ static hasListener = false;
+
+ /**
+ * A callback function to run when the element is clicked.
+ * @param {Event} event The DOM event that triggers the listener
+ * @returns {void}
+ */
+ static listener(event) {}
+
+ /**
+ * Add Event listeners to the DOM body at startup.
+ */
+ static addListeners() {
+ const body = $("body");
+ body.on("click", `i.repost`, this.repostListener);
+ for (const [action, cls] of Object.entries(CONFIG.SFRPG.enricherTypes)) {
+ if (cls.hasListener) body.on("click", `a[data-action="${action}"]`, cls.listener);
+ }
+ }
+}
diff --git a/src/module/bugfix-code/system/enrichers/check.js b/src/module/bugfix-code/system/enrichers/check.js
new file mode 100644
index 0000000..b29b37d
--- /dev/null
+++ b/src/module/bugfix-code/system/enrichers/check.js
@@ -0,0 +1,141 @@
+import CheckNameHelper from "../../utils/skill-names.js";
+import BaseEnricher from "./base.js";
+
+/**
+ * Roll a specific check
+ * @class
+ */
+export class CheckEnricher extends BaseEnricher {
+ // @Check[type:athletics]
+ // @Check[type:life-science]
+ // @Check[type:reflex]
+ constructor() {
+ super();
+ }
+
+ /** @inheritdoc */
+ get enricherType() {
+ return "Check";
+ }
+
+ /** @inheritdoc */
+ get validTypes() {
+ return [
+ ...Object.keys(CONFIG.SFRPG.skills),
+ ...Object.keys(CONFIG.SFRPG.saves),
+ ...Object.keys(CONFIG.SFRPG.abilities),
+ "caster-level"
+ ];
+ }
+
+ /** @inheritdoc */
+ get icons() {
+ return {
+ "acrobatics": "fa-person-walking",
+ "athletics": "fa-dumbbell",
+ "bluff": "fa-comment",
+ "computers": "fa-computer",
+ "culture": "fa-flag",
+ "diplomacy": "fa-handshake",
+ "disguise": "fa-mask",
+ "engineering": "fa-gear",
+ "intimidate": "fa-face-angry",
+ "life-science": "fa-dna",
+ "medicine": "fa-syringe",
+ "mysticism": "fa-hand-sparkles",
+ "perception": "fa-magnifying-glass",
+ "profession": "fa-user-tie",
+ "physical-science": "fa-flask",
+ "piloting": "fa-plane",
+ "sense-motive": "fa-person-circle-question",
+ "sleight-of-hand": "fa-hands",
+ "stealth": "fa-moon",
+ "survival": "fa-campground",
+
+ "fortitude": "fa-shield-heart",
+ "reflex": "fa-person-running",
+ "will": "fa-brain",
+
+ "strength": "fa-weight-hanging",
+ "dexterity": "fa-feather-pointed",
+ "constitution": "fa-heart-pulse",
+ "intelligence": "fa-glasses",
+ "wisdom": "fa-mountain-sun",
+ "charisma": "fa-people-arrows",
+
+ "caster-level": "fa-wand-magic-sparkles"
+ };
+ }
+
+ get checkType() {
+ const shortName = CheckNameHelper.shortFormName(this.args.type);
+ const C = CONFIG.SFRPG;
+
+ if (shortName in C.skills) return "skill";
+ else if (shortName in C.saves) return "save";
+ else if (shortName in C.abilities) return "ability";
+ else return null;
+ }
+
+ get localizedType() {
+ const C = CONFIG.SFRPG;
+ const { type } = this.args;
+
+ switch (this.checkType) {
+ case "skill": return C.skills[type];
+ case "save": return C.saves[type];
+ case "ability": return C.abilities[type];
+ default: return "";
+ }
+ }
+
+ /**
+ * @override to check using the 3-letter identifier for the type against the valid types (which are 3 letter identifiers).
+ * Inputted types are full names.
+ */
+ isValid() {
+ if (!this.args.type || !this.validTypes.includes(CheckNameHelper.shortFormName(this.args.type))) {
+ return this._failValidation("Type");
+ }
+
+ return true;
+ }
+
+ validateName() {
+ const i18nPath = this.checkType === save ? "SFRPG.Save" : "SFRPG.Check";
+ const localizedCheck = game.i18n.localize(i18nPath);
+
+ this.name ||= `${this.localizedType} ${localizedCheck}`;
+ }
+
+ /**
+ * @extends BaseEnricher
+ * @returns {HTMLAnchorElement} */
+ createElement() {
+ const a = super.createElement();
+
+ if (this.args.dc) a.dataset.dc = parseInt(this.args.dc);
+
+ a.innerHTML = `${a.innerHTML}`;
+
+ return a;
+
+ }
+
+ static hasRepost = true;
+ static hasListener = true;
+
+ static listener(event) {
+ const data = event.currentTarget.dataset;
+
+ const actor = _token?.actor ?? game.user?.character;
+ if (!actor) return ui.notifications.error("You must have a token or an actor selected.");
+ const id = CheckNameHelper.shortFormName(data.type);
+
+ if (id in CONFIG.SFRPG.skills) actor.rollSkill(id);
+ else if (id in CONFIG.SFRPG.saves) actor.rollSave(id);
+ else if (id in CONFIG.SFRPG.abilities) actor.rollAbility(id);
+
+ }
+
+}
diff --git a/src/module/test.js b/src/module/test.js
new file mode 100644
index 0000000..5c1d776
--- /dev/null
+++ b/src/module/test.js
@@ -0,0 +1,5 @@
+export class test {
+ logText() {
+ console.log('Test Log.');
+ }
+}
\ No newline at end of file
diff --git a/src/module/utils/skill-names.js b/src/module/utils/skill-names.js
new file mode 100644
index 0000000..c372239
--- /dev/null
+++ b/src/module/utils/skill-names.js
@@ -0,0 +1,100 @@
+
+/**
+ * @class
+ * Helper class to transform shortform ability names back and forth between their full names.
+ */
+export class CheckNameHelper {
+ /**
+ * Take the full name for a check, and return the 3-letter identifier
+ * @param {String} fullName The full name for the check, such as "life-science" or "acrobatics"
+ * @returns {String} The 3-letter name, if it exists, otherwise the inputted full name
+ */
+ static shortFormName(fullName) {
+ return {
+ "acrobatics": "acr",
+ "athletics": "ath",
+ "bluff": "blu",
+ "computers": "com",
+ "culture": "cul",
+ "diplomacy":"dip",
+ "disguise": "dis",
+ "engineering":"eng",
+ "intimidate": "int",
+ "life-science":"lsc",
+ "medicine": "med",
+ "mysticism": "mys",
+ "perception": "per",
+ "profession": "pro",
+ "physical-science":"phs",
+ "piloting": "pil",
+ "sense-motive":"sen",
+ "sleight-of-hand":"sle",
+ "stealth": "ste",
+ "survival": "sur",
+
+ "fortitude": "fort",
+ "reflex": "reflex",
+ "will": "will",
+
+ "strength": "str",
+ "dexterity": "dex",
+ "constitution": "con",
+ "intelligence": "int",
+ "wisdom": "wis",
+ "charisma": "cha",
+
+ "caster-level": "caster-level"
+ }[fullName] || fullName;
+ }
+
+ /**
+ * Take the 3-letter identifier for a check, and return the full name
+ * @param {String} threeLetter The 3-letter identifier for the check, such as "lsc" or "acr"
+ * @returns {String} The full name, if it exists, otherwise the inputted 3-letter name
+ */
+ static longFormName(threeLetter) {
+ return {
+ "acr": "acrobatics",
+ "ath": "athletics",
+ "blu": "bluff",
+ "com":"computers",
+ "cul": "culture",
+ "dip": "diplomacy",
+ "dis": "disguise",
+ "eng": "engineering",
+ "int": "intimidate",
+ "lsc": "life-science",
+ "med": "medicine",
+ "mys": "mysticism",
+ "per": "perception",
+ "pro": "profession",
+ "phs": "physical-science",
+ "pil": "piloting",
+ "sen": "sense-motive",
+ "sle": "sleight-of-hand",
+ "ste": "stealth",
+ "sur": "survival",
+
+ "fort": "fortitude",
+ "reflex": "reflex",
+ "will": "will",
+
+ "caster-level": "caster-level"
+ }[threeLetter] || threeLetter;
+ }
+
+ /**
+ * Same as longformName, but a seperate function to stop namespace collision between the "int" of intimidate and intelligence
+ * @see longFormName
+ */
+ static longFormNameAbilities(threeLetter) {
+ return {
+ "str": "strength",
+ "dex": "dexterity",
+ "con": "constitution",
+ "int": "intelligence",
+ "wis": "wisdom",
+ "cha": "charisma"
+ }[threeLetter] || threeLetter;
+ }
+}
From cf9fcb3eac5a60b17fb404d227f942593a45c5a3 Mon Sep 17 00:00:00 2001
From: Ian Bennett <34078802+ian612@users.noreply.github.com>
Date: Thu, 26 Dec 2024 13:21:45 -0500
Subject: [PATCH 2/3] Cleanup files for release
---
.../bugfix-code/system/enrichers/base.js | 233 ------------------
.../bugfix-code/system/enrichers/check.js | 141 -----------
src/module/test.js | 5 -
src/module/utils/skill-names.js | 100 --------
4 files changed, 479 deletions(-)
delete mode 100644 src/module/bugfix-code/system/enrichers/base.js
delete mode 100644 src/module/bugfix-code/system/enrichers/check.js
delete mode 100644 src/module/test.js
delete mode 100644 src/module/utils/skill-names.js
diff --git a/src/module/bugfix-code/system/enrichers/base.js b/src/module/bugfix-code/system/enrichers/base.js
deleted file mode 100644
index eebd349..0000000
--- a/src/module/bugfix-code/system/enrichers/base.js
+++ /dev/null
@@ -1,233 +0,0 @@
-/**
- * @typedef {Object} CustomEnricher
- * @property {RegExp} pattern
- * @property {EnricherFunction} enricher
- */
-
-/**
- * Abstract base class for enrichers which carries validation and basic element creation.
- * @abstract
- * @class
- */
-export class BaseEnricher {
-
- /** @type {CustomEnricher} */
- constructor() {
- if (this.constructor === BaseEnricher) throw new Error(
- "The BaseEnricher class is an abstract class and may not be instantiated."
- );
- this.pattern = this.regex;
- this.enricher = this.enricherFunc.bind(this);
- }
-
- /** --------
- | |
- | Getters |
- | |
- ----------*/
-
- /**
- * The RegExp to capture the text.
- * @returns {RegExp}
- */
- get regex() {
- return new RegExp(`(@${this.enricherType})(\\[[^\\]]+)](?:{([^}]+)})?`, "gm");
- }
-
- /**
- * The type of custom enricher, i.e the word following the @
- * @returns {String}
- */
- get enricherType() {
- throw new Error("This method must be implemented on subclasses of BaseEnricher.");
- }
-
- /**
- * Valid options for the type argument
- * @returns {String[]}
- */
- get validTypes() {
- throw new Error("This method must be implemented on subclasses of BaseEnricher.");
- }
-
- /**
- * An object of FA icons to be used in the element
- * @returns {Object}
- */
- get icons() {
- throw new Error("This method must be implemented on subclasses of BaseEnricher.");
- }
-
- /** -------------------
- | |
- | Element Generation |
- | |
- ----------------------*/
-
- /**
- * Transform the Regex match array into an enriched element, performing validation.
- * @callback EnricherFunction
- * @param {RegExp} match A Regex match array from the inputted text
- * @param {Object} options
- * @returns {HTMLElement} The enriched element
- */
- enricherFunc(match, options) {
- this.match = match;
-
- if (this.match[3]) this.name = this.match[3];
- else this.name = undefined;
-
- this.parseArgs();
-
- // Early return an error element if invalid
- if (!this.isValid()) return this.element;
-
- this.validateName();
-
- this.element = this.createElement();
-
- return this.element;
- }
-
- /**
- * Transform the args in the orginal text to an object
- */
- parseArgs() {
- // Split each argument from the square brackets
- const args = this.match[2].split("|");
-
- this.args = args.reduce((obj, i) => {
- // Split each arg into a key and a value
- // Matches a colon with a letter before, and either a JSON or character after.
- // Set up as to not split colons in JSONs
- const split = i.match(/(\w*):({.*}?|.+)/);
- if (split?.length > 0) obj[split[1]] = split[2];
-
- return obj;
- }, {});
- }
-
- /**
- * Checks if there is a type argument, and that it is valid for the enricher's type.
- * Sets this.element if invalid for an early return.
- * @returns {Boolean}
- */
- isValid() {
- if (!this.args.type || !this.validTypes.includes(this.args.type)) {
- return this._failValidation("Type");
- }
-
- return true;
- }
-
- /**
- * Create an error element after isValid() fails
- * @param {String} failedArg The argument that failed validation, to be used in the error element
- * @returns {false}
- */
- _failValidation(failedArg) {
- const strong = document.createElement("strong");
- strong.innerText = `${this.enricherType} parsing failed! ${failedArg} is invalid.`;
- this.element = strong;
- return false;
- }
-
- /**
- * Sets a default name if none was given
- */
- validateName() {
- this.name ||= `${this.args.type.capitalize()} ${this.enricherType}`;
- }
-
- /**
- * Create a HTML element and affix some data.
- * Can be called in subclasses by assigning the super to a local variable.
- * @returns {HTMLAnchorElement}
- */
- createElement() {
- let a = document.createElement("a");
-
- a.dataset.action = this.enricherType;
- a.dataset.type = this.args.type;
-
- a.classList.add("enriched-link");
- a.draggable = false;
-
- a.innerText = this.name;
-
- if (this.#_hasRepost) a = this.addRepost(a);
-
- return a;
- }
-
- /** -------
- | |
- | Repost |
- | |
- -----------*/
-
- /**
- * Should this enricher have a repost button appended to created elements?
- * Create both a publicly accessible static variable and an internal instance one.
- * @type {Boolean}
- */
- static hasRepost = false;
- /** @type {Boolean} */
- #_hasRepost = this.constructor.hasRepost;
-
- /**
- * Take an anchor element and append a repost button
- * @param {HTMLAnchorElement} a The original anchor
- * @returns The inputted Anchor, with a repost button appended
- */
- addRepost(a) {
- const repost = document.createElement("i");
- repost.classList.add("fas", "fa-comment-alt", "repost");
- repost.dataset.tooltip = "SFRPG.Enrichers.SendToChat";
-
- a.append(repost);
-
- return a;
- }
-
- /**
- * Handle repost button click, sending a chat message of the current target to chat.
- * @param {Event} event
- * @returns Create a chat message
- */
- static repostListener(event) {
- event.stopPropagation();
-
- return ChatMessage.create({content: event.currentTarget.parentElement.outerHTML});
- }
-
- /** ---------
- | |
- | Listener |
- | |
- ------------*/
-
- /**
- * Whether the enricher has an event listener.
- * @type {Boolean}
- */
- static hasListener = false;
-
- /**
- * A callback function to run when the element is clicked.
- * @param {Event} event The DOM event that triggers the listener
- * @returns {void}
- */
- static listener(event) {}
-
- /**
- * Add Event listeners to the DOM body at startup.
- */
- static addListeners() {
- const body = $("body");
- body.on("click", `i.repost`, this.repostListener);
- for (const [action, cls] of Object.entries(CONFIG.SFRPG.enricherTypes)) {
- if (cls.hasListener) body.on("click", `a[data-action="${action}"]`, cls.listener);
- }
- }
-}
diff --git a/src/module/bugfix-code/system/enrichers/check.js b/src/module/bugfix-code/system/enrichers/check.js
deleted file mode 100644
index b29b37d..0000000
--- a/src/module/bugfix-code/system/enrichers/check.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import CheckNameHelper from "../../utils/skill-names.js";
-import BaseEnricher from "./base.js";
-
-/**
- * Roll a specific check
- * @class
- */
-export class CheckEnricher extends BaseEnricher {
- // @Check[type:athletics]
- // @Check[type:life-science]
- // @Check[type:reflex]
- constructor() {
- super();
- }
-
- /** @inheritdoc */
- get enricherType() {
- return "Check";
- }
-
- /** @inheritdoc */
- get validTypes() {
- return [
- ...Object.keys(CONFIG.SFRPG.skills),
- ...Object.keys(CONFIG.SFRPG.saves),
- ...Object.keys(CONFIG.SFRPG.abilities),
- "caster-level"
- ];
- }
-
- /** @inheritdoc */
- get icons() {
- return {
- "acrobatics": "fa-person-walking",
- "athletics": "fa-dumbbell",
- "bluff": "fa-comment",
- "computers": "fa-computer",
- "culture": "fa-flag",
- "diplomacy": "fa-handshake",
- "disguise": "fa-mask",
- "engineering": "fa-gear",
- "intimidate": "fa-face-angry",
- "life-science": "fa-dna",
- "medicine": "fa-syringe",
- "mysticism": "fa-hand-sparkles",
- "perception": "fa-magnifying-glass",
- "profession": "fa-user-tie",
- "physical-science": "fa-flask",
- "piloting": "fa-plane",
- "sense-motive": "fa-person-circle-question",
- "sleight-of-hand": "fa-hands",
- "stealth": "fa-moon",
- "survival": "fa-campground",
-
- "fortitude": "fa-shield-heart",
- "reflex": "fa-person-running",
- "will": "fa-brain",
-
- "strength": "fa-weight-hanging",
- "dexterity": "fa-feather-pointed",
- "constitution": "fa-heart-pulse",
- "intelligence": "fa-glasses",
- "wisdom": "fa-mountain-sun",
- "charisma": "fa-people-arrows",
-
- "caster-level": "fa-wand-magic-sparkles"
- };
- }
-
- get checkType() {
- const shortName = CheckNameHelper.shortFormName(this.args.type);
- const C = CONFIG.SFRPG;
-
- if (shortName in C.skills) return "skill";
- else if (shortName in C.saves) return "save";
- else if (shortName in C.abilities) return "ability";
- else return null;
- }
-
- get localizedType() {
- const C = CONFIG.SFRPG;
- const { type } = this.args;
-
- switch (this.checkType) {
- case "skill": return C.skills[type];
- case "save": return C.saves[type];
- case "ability": return C.abilities[type];
- default: return "";
- }
- }
-
- /**
- * @override to check using the 3-letter identifier for the type against the valid types (which are 3 letter identifiers).
- * Inputted types are full names.
- */
- isValid() {
- if (!this.args.type || !this.validTypes.includes(CheckNameHelper.shortFormName(this.args.type))) {
- return this._failValidation("Type");
- }
-
- return true;
- }
-
- validateName() {
- const i18nPath = this.checkType === save ? "SFRPG.Save" : "SFRPG.Check";
- const localizedCheck = game.i18n.localize(i18nPath);
-
- this.name ||= `${this.localizedType} ${localizedCheck}`;
- }
-
- /**
- * @extends BaseEnricher
- * @returns {HTMLAnchorElement} */
- createElement() {
- const a = super.createElement();
-
- if (this.args.dc) a.dataset.dc = parseInt(this.args.dc);
-
- a.innerHTML = `${a.innerHTML}`;
-
- return a;
-
- }
-
- static hasRepost = true;
- static hasListener = true;
-
- static listener(event) {
- const data = event.currentTarget.dataset;
-
- const actor = _token?.actor ?? game.user?.character;
- if (!actor) return ui.notifications.error("You must have a token or an actor selected.");
- const id = CheckNameHelper.shortFormName(data.type);
-
- if (id in CONFIG.SFRPG.skills) actor.rollSkill(id);
- else if (id in CONFIG.SFRPG.saves) actor.rollSave(id);
- else if (id in CONFIG.SFRPG.abilities) actor.rollAbility(id);
-
- }
-
-}
diff --git a/src/module/test.js b/src/module/test.js
deleted file mode 100644
index 5c1d776..0000000
--- a/src/module/test.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export class test {
- logText() {
- console.log('Test Log.');
- }
-}
\ No newline at end of file
diff --git a/src/module/utils/skill-names.js b/src/module/utils/skill-names.js
deleted file mode 100644
index c372239..0000000
--- a/src/module/utils/skill-names.js
+++ /dev/null
@@ -1,100 +0,0 @@
-
-/**
- * @class
- * Helper class to transform shortform ability names back and forth between their full names.
- */
-export class CheckNameHelper {
- /**
- * Take the full name for a check, and return the 3-letter identifier
- * @param {String} fullName The full name for the check, such as "life-science" or "acrobatics"
- * @returns {String} The 3-letter name, if it exists, otherwise the inputted full name
- */
- static shortFormName(fullName) {
- return {
- "acrobatics": "acr",
- "athletics": "ath",
- "bluff": "blu",
- "computers": "com",
- "culture": "cul",
- "diplomacy":"dip",
- "disguise": "dis",
- "engineering":"eng",
- "intimidate": "int",
- "life-science":"lsc",
- "medicine": "med",
- "mysticism": "mys",
- "perception": "per",
- "profession": "pro",
- "physical-science":"phs",
- "piloting": "pil",
- "sense-motive":"sen",
- "sleight-of-hand":"sle",
- "stealth": "ste",
- "survival": "sur",
-
- "fortitude": "fort",
- "reflex": "reflex",
- "will": "will",
-
- "strength": "str",
- "dexterity": "dex",
- "constitution": "con",
- "intelligence": "int",
- "wisdom": "wis",
- "charisma": "cha",
-
- "caster-level": "caster-level"
- }[fullName] || fullName;
- }
-
- /**
- * Take the 3-letter identifier for a check, and return the full name
- * @param {String} threeLetter The 3-letter identifier for the check, such as "lsc" or "acr"
- * @returns {String} The full name, if it exists, otherwise the inputted 3-letter name
- */
- static longFormName(threeLetter) {
- return {
- "acr": "acrobatics",
- "ath": "athletics",
- "blu": "bluff",
- "com":"computers",
- "cul": "culture",
- "dip": "diplomacy",
- "dis": "disguise",
- "eng": "engineering",
- "int": "intimidate",
- "lsc": "life-science",
- "med": "medicine",
- "mys": "mysticism",
- "per": "perception",
- "pro": "profession",
- "phs": "physical-science",
- "pil": "piloting",
- "sen": "sense-motive",
- "sle": "sleight-of-hand",
- "ste": "stealth",
- "sur": "survival",
-
- "fort": "fortitude",
- "reflex": "reflex",
- "will": "will",
-
- "caster-level": "caster-level"
- }[threeLetter] || threeLetter;
- }
-
- /**
- * Same as longformName, but a seperate function to stop namespace collision between the "int" of intimidate and intelligence
- * @see longFormName
- */
- static longFormNameAbilities(threeLetter) {
- return {
- "str": "strength",
- "dex": "dexterity",
- "con": "constitution",
- "int": "intelligence",
- "wis": "wisdom",
- "cha": "charisma"
- }[threeLetter] || threeLetter;
- }
-}
From bf691b6d55c426472b907518c6bafc36aede01cf Mon Sep 17 00:00:00 2001
From: Ian Bennett <34078802+ian612@users.noreply.github.com>
Date: Thu, 26 Dec 2024 13:21:57 -0500
Subject: [PATCH 3/3] Add setting to enable/disable the bugfix code
---
src/init.mjs | 23 +++++++++++++++++------
src/lang/en.json | 4 ++++
2 files changed, 21 insertions(+), 6 deletions(-)
diff --git a/src/init.mjs b/src/init.mjs
index 3603de2..684793d 100644
--- a/src/init.mjs
+++ b/src/init.mjs
@@ -17,12 +17,23 @@ const itemSizeArmorClassModifier = {
Hooks.once("init", () => {
preloadHandlebarsTemplates()
Items.registerSheet(MODULE_ID, EnhancedItemSheetMixin(game.sfrpg.applications.ItemSheetSFRPG), {makeDefault: true})
- const test = new CheckEnricher();
- console.log("------------------------------------------------------------------------------------------------------------");
- console.log(CONFIG);
- console.log(test);
- CONFIG.TextEditor.enrichers[2] = new CheckEnricher();
- // CONFIG.TextEditor.enrichers.push(new BrowserEnricher(), new IconEnricher(), new CheckEnricher(), new TemplateEnricher());
+
+ // Hotfix for inline check bug
+ // This is really hacky and won't be needed in SFRPG version 27, which includes this bugfix in a more stable way
+ // That's okay because this module is also deprecated in SFRPG version 27, so it won't matter, really
+ game.settings.register("sfrpg-item-sheets", "enable-inline-check-bugfix", {
+ name: "SFRPGItemSheets.Settings.InlineCheckBugfixName",
+ hint: "SFRPGItemSheets.Settings.InlineCheckBugfixHint",
+ scope: "world",
+ config: true,
+ default: true,
+ type: Boolean,
+ requiresReload: true
+ });
+
+ if (game.settings.get("sfrpg-item-sheets", "enable-inline-check-bugfix")) {
+ CONFIG.TextEditor.enrichers[2] = new CheckEnricher();
+ }
})
function EnhancedItemSheetMixin(SheetClass) {
diff --git a/src/lang/en.json b/src/lang/en.json
index 4674947..a71962a 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -116,6 +116,10 @@
},
"RangeInformation": "Range Information"
},
+ "Settings": {
+ "InlineCheckBugfixName": "Enable Bugfix for Inline Skill Checks",
+ "InlineCheckBugfixHint": "Checking this setting will enable a small bugfix for inline checks in SFRPG system version 26"
+ },
"SheetLabel": "Enhanced Item Sheet"
}
}
\ No newline at end of file