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