From dde3cd5b13a587b7130999555d270554b93aa831 Mon Sep 17 00:00:00 2001 From: Geekswordsman Date: Wed, 1 Mar 2023 11:32:45 -0500 Subject: [PATCH] resolves #103, refs #140, refs #82, refs #64, refs #53, refs #49, resolves #38, resolves #14 --- lang/en.json | 59 ++- less/token-mold.less | 6 +- module/config.mjs | 1 + .../forms/token-mold-configuration-dialog.mjs | 410 ++++++++++-------- module/models/token-mold-mappings.mjs | 3 + .../token-mold-rule-config.mjs} | 71 ++- module/models/token-mold-rule.mjs | 26 ++ module/token-mold.mjs | 190 ++++++-- module/utils/namegenerator.mjs | 278 +++++++++++- styles/token-mold.css | 5 +- templates/config.hbs | 30 +- templates/partials/hp-tab.hbs | 1 + templates/partials/name-tab.hbs | 184 ++++++++ 13 files changed, 966 insertions(+), 298 deletions(-) create mode 100644 module/models/token-mold-mappings.mjs rename module/{utils/token-mold-config.mjs => models/token-mold-rule-config.mjs} (58%) create mode 100644 module/models/token-mold-rule.mjs create mode 100644 templates/partials/hp-tab.hbs create mode 100644 templates/partials/name-tab.hbs diff --git a/lang/en.json b/lang/en.json index 2077137..f2e144e 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,23 +1,25 @@ { - "I18N.MAINTAINERS": [ "Moerill", "Geekswordsman" ], + "I18N.MAINTAINERS": [ "Geekswordsman", "Moerill" ], "Token Mold": "Token Mold", "TOKEN-MOLD": { "ABOUT": { "About": "About Token Mold", - "CreatedBy": "Maintained By", + "CreatedBy": "V3 and later created by", "Help": "Help", "LiveSupport": "For live support of Token Mold, join the Foundry VTT Discord and post any questions in #module-troubleshooting!", - "OriginallyCreatedBy": "Originally Created By", + "OriginallyCreatedBy": "Based on the original code by", "ProjectPage": "View the Project Page to report issues or comment on this Project at: ", "SupportTheCreator": "If you enjoy Token, please consider donating to the creator at: ", "Title": "Token Mold", "Wiki": "View the Token Mold wiki at: " }, "CONFIG": { + "DispositionSelect": "Select Token Disposition", "Friendly": "Friendly", "Hostile": "Hostile", "Linked": "Linked", + "LinkedOnly": "Linked Tokens Only?", "Neutral": "Neutral", "Save": "Save and Close", "Unlinked": "Unlinked" @@ -29,6 +31,52 @@ "Overlay": "Overlay", "Settings": "Settings" }, + "NAME": { + "AddAdjective": "Add random adjective from dictionary", + "AddAttribute": "Add Attribute", + "AddCustomPrefix": "Add the following custom word to beginning of the name:", + "AddCustomSuffix": "Add the following custom word after the name:", + "AddNumbers": "Add counting numbers to name as Suffix.", + "AddValue": "Add Value", + "AdjectiveBack": "Back", + "AdjectiveFront": "Front", + "AdjectivePlacement": "Adjective Placement", + "AdjectiveUse": "Enable Adjective Options?", + "Attribute": "Attribute", + "BaseName": "Base name:", + "BaseNameNothing": "Do nothing", + "BaseNameRemove": "Remove", + "BaseNameReplace": "Replace with randomized name", + "BaseNameOverride": "Hold Shift to override name removal or replacement and always add the base name", + "ChooseRolltable": "Choose adjectives from following rollable Table:", + "Default": "Default", + "Delete": "Delete", + "GenerateLength": "Generated Name length", + "IncreaseIndex": "Increase Index by up to", + "IncreaseIndexHelp": "The index increase will be chosen randomly from within this range. Minimum of 1. (Uniformly distributed)", + "Language": "Language", + "LanguageOrTable": "Language/Table", + "Maximum": "Maximum:", + "Minimum": "Minimum:", + "Regenerate": "Regenerate names for selected tokens", + "NameOptions": "Name Options", + "NumberStyle": "Choose number style. The first field may not be empty!", + "Prefix": "Prefix", + "PrefixHelp": "Configure options that happen before the token's name.", + "PrefixOptions": "Prefix Options", + "Random": "random", + "ReplaceHelp": "For more information on how this works look onto the modules homepage.", + "ReplaceInfo1": "The chosen option will replace the tokens base name with a generated name.", + "ReplaceInfo2": "You can choose a number of attributes. For each attribute you can choose possible values and assign them a language. On token creation the first value that fits will be chosen. If no fitting value was found, the default will be used.", + "ReplaceInfo3": "Important: Enabling this feature will result in about 100MB of extra memory used! (for GMs only)", + "Reset": "Reset to default", + "ResetCounter": "Reset counter for this scene for all tokens.", + "ResetCounterHelp": "Resets the Counter for newly created tokens. This will reset to the highest placed index after a page reload. On maps with a high amount of tokens this could take a few seconds.", + "RollTable": "Rollable Table", + "Suffix": "Suffix", + "SuffixOptions": "Suffix Options", + "Value": "Value" + }, "SETTINGS": { "Config": "Token Mold Configuration", "ConfigHint": "Configure Settings for Token Mold. Also available at the top of the Actor Tab.", @@ -41,7 +89,12 @@ "Reapply": "Reapplies all settings to selected tokens as if those were replaced onto the scene.", "Token": "(De-)activate Token Config Overwrite" }, + "STAT": { + "None": "None", + "Name": "Name" + }, "TAB": { + "HP": "Hit Points", "Name": "Name", "SystemSpecific": "System Specific", "TokenConfig": "Token Config", diff --git a/less/token-mold.less b/less/token-mold.less index 8b43475..1049a38 100644 --- a/less/token-mold.less +++ b/less/token-mold.less @@ -50,6 +50,10 @@ } /* Config */ + .tmold_custom_word_label { + flex: 8; + } + ul.token-mold-config-tabs { list-style-type: none; margin: 0px; @@ -86,7 +90,7 @@ padding: 2px 6px; } - li.token-mod-active-config { + li.token-mold-active-config { color: black; text-shadow: 0 0 8px var(--color-shadow-primary); } diff --git a/module/config.mjs b/module/config.mjs index ec214d8..7f4b6a0 100644 --- a/module/config.mjs +++ b/module/config.mjs @@ -8,6 +8,7 @@ CONFIG.SYSTEM_SUPPORTED = false; CONFIG.HP_SUPPORTED = false; CONFIG.NEW_SETTINGS = {}; CONFIG.SETTINGS = {}; +CONFIG.COUNTERS = []; CONFIG.BAR_ATTRIBUTES = []; CONFIG.ADJECTIVES = null; CONFIG.DICTIONARY = null; diff --git a/module/forms/token-mold-configuration-dialog.mjs b/module/forms/token-mold-configuration-dialog.mjs index 6393272..bcbe71b 100644 --- a/module/forms/token-mold-configuration-dialog.mjs +++ b/module/forms/token-mold-configuration-dialog.mjs @@ -4,28 +4,30 @@ import { Logger } from "../logger/logger.mjs"; import { TokenMold } from "../token-mold.mjs"; export class TokenMoldConfigurationDialog extends HelpFormApplication { + #ruleConfig = null; constructor(object, options) { if (!object) { object = {} } object.enableAboutButton = true; super(object, options); + this.#ruleConfig = object?.ruleConfig ?? CONFIG.SETTINGS.CONFIGURATIONS.first(); this.data = CONFIG.SETTINGS; this.barAttributes = CONFIG.BAR_ATTRIBUTES || []; - this.activeConfig = "Unlinked"; } static get defaultOptions() { const options = super.defaultOptions; options.template = "modules/token-mold/templates/config.hbs"; options.width = 420; - options.height = 460; + // options.height = 460; options.resizable = false; options.classes = ["token-mold"]; options.title = "Token Mold"; options.closeOnSubmit = false; options.submitOnClose = true; - options.submitOnChange = false; - options.scrollY = ["section.content"] + options.submitOnChange = true; + options.scrollY = ["section.content"]; +// options.dragDrop = [{ dropSelector: '.token-mold-config-tabs', dragSelector: '.token-mold-draggable-tab' }]; options.tabs = [ { navSelector: ".tabs", @@ -37,183 +39,22 @@ export class TokenMoldConfigurationDialog extends HelpFormApplication { return options; } - _getHeaderButtons() { - let btns = super._getHeaderButtons(); - Logger.debug(false, "HEADER BUTTONS!", btns); - btns.find(b => b.class === "close").label = game.i18n.localize("TOKEN-MOLD.CONFIG.Save"); - return btns; + async close(options={}) { + super.close({force: true}); } - async _onSubmit(ev) { - const attrGroups = $(this.form).find(".attributes"); - let attrs = []; - attrGroups.each((idx, e) => { - const el = $(e); - const icon = el.find(".icon").val(), - value = el.find(".value").val(); - if (icon !== "" && value !== "") - attrs.push({ - icon: icon, - path: value, - }); - }); - this.data.overlay.attrs = attrs; - - this.data.name.options.default = this.form - .querySelector(".default-group") - .querySelector(".language").value; - const attributes = []; - - const langAttrGroups = this.form.querySelectorAll(".attribute-selection"); - - langAttrGroups.forEach((el) => { - let ret = { languages: {} }; - ret.attribute = el.querySelector(".attribute").value; - el.querySelectorAll(".language-group").forEach((langGroup) => { - ret.languages[langGroup.querySelector(".value").value.toLowerCase()] = - langGroup.querySelector(".language").value; - }); - attributes.push(ret); - }); - - this.data.name.options.attributes = attributes; - super._onSubmit(ev); - } - - async _updateObject(event, formData) { - let min = formData["name.options.min"], - max = formData["name.options.max"]; - if (min < 0) min = 0; - if (max < 0) max = 0; - - if (min > max) { - const tmp = min; - (min = max), (max = tmp); - } - - formData["name.options.min"] = min; - formData["name.options.max"] = max; - - // For name prefix and suffix, if the value is only a space the formData doesn't pick it up, so check and manually set prior to merge. - let prefix = $(this.form).find("input[name='name.number.prefix']").val(); - let suffix = $(this.form).find("input[name='name.number.suffix']").val(); - formData["name.number.prefix"] = formData["name.number.prefix"] !== prefix ? prefix : formData["name.number.prefix"]; - formData["name.number.suffix"] = formData["name.number.suffix"] !== suffix ? suffix : formData["name.number.suffix"]; - - this.object.data = mergeObject(this.data, formData); - - if (this._resetOptions === true) { - // this.object.data.name.options = this.object.dndDefaultNameOptions; - - const dndOptions = this.object.dndDefaultNameOptions; - this.object.data.name.options.default = dndOptions.default; - this.object.data.name.options.attributes = dndOptions.attributes; - this._resetOptions = false; - - this.render(); - } - - TokenMold.SaveSettings(); - } - - getData() { - let data = { - data: this.data, - activeConfig: this.activeConfig - }; - data.numberStyles = { - ar: "arabic numerals", - alu: "alphabetic UPPER", - all: "alphabetic LOWER", - ro: "roman numerals", - }; - data.barAttributes = this.barAttributes; - data.actorAttributes = this._actorAttributes; - data.displayModes = CONST.TOKEN_DISPLAY_MODES; - data.dispositions = CONST.TOKEN_DISPOSITIONS; - data.defaultIcons = this.defaultIcons; - data.showCreatureSize = /dnd5e|pf2e/.exec(game.data.system.id) !== null - data.showHP = CONFIG.HP_SUPPORTED; - data.showSystem = CONFIG.SYSTEM_SUPPORTED; - data.languages = CONFIG.LANGUAGES; - data.rollTableList = CONFIG.ROLLTABLES; - data.visionLabel = game.i18n.localize("TOKEN.VisionEnabled") - - Logger.debug(false, "Prepared data", data, CONFIG.ROLLTABLES); - return data; - } - - static get defaultAttrs() { - if (/dnd5e|sw5e/.exec(game.data.system.id) !== null) { - return [ - { - value: "data.attributes.ac.value", - label: "Armor Class", - icon: '', - }, - { - value: "data.skills.prc.passive", - label: "Passive Perception", - icon: '', - }, - ]; - } else - return [ - { - icon: '', - value: "", - }, - ]; - } - - get defaultIcons() { - return [ - "", // eye - " ", //fas fa-shield-alt">', - " ", //fas fa-dice-d20">', - " ", //fas fa-heartbeat">', - " ", //fas fa-hat-wizard">', - " ", //fas fa-shoe-prints">', - " ", //fas fa-walking">', - " ", //fas fa-running">', - " ", //fas fa-coins">', - " ", //fas fa-poop">', - " ", //fas fa-shopping-bag">', - "", //fas fa-money-bill-wave">', - "", // fas fa-suitcase">', - "", //fas fa-fire">', - "", //fas fa-paw">', - "", //fas fa-carrot">', - "", //fas fa-bone">', - "", //fas fa-drumstick-bite">', - "", //fas fa-apple-alt">', - "", //fas fa-fist-raised">', - "", //fas fa-jedi">', - "", //fas fa-meteor">', - "", //fas fa-moon">', - "", //fas fa-rocket">' - "", // brain - "", // child - ]; - } - - get languages() { - return this.object.languages; - } + // _getHeaderButtons() { + // let btns = super._getHeaderButtons(); + // Logger.debug(false, "HEADER BUTTONS!", btns); + // btns.find(b => b.class === "close").label = game.i18n.localize("TOKEN-MOLD.CONFIG.Save"); + // return btns; + // } activateListeners(html) { super.activateListeners(html); html.on('click', '[data-action]', this.#handleAction.bind(this)); - /* - html.find(".add-attribute").on("click", (ev) => { - const addBtn = $(ev.target); - const clone = addBtn.prev().clone(); - clone.find("select").val(""); - addBtn.before(clone); - }); - html.on("click", ".remove", (ev) => { const container = $(ev.currentTarget).closest(".form-group"); @@ -324,6 +165,187 @@ export class TokenMoldConfigurationDialog extends HelpFormApplication { */ } + getData() { + let data = { + data: this.data, + activeConfig: this.#ruleConfig + }; + data.numberStyles = { + ar: "arabic numerals", + alu: "alphabetic UPPER", + all: "alphabetic LOWER", + ro: "roman numerals", + }; + data.barAttributes = this.barAttributes; + data.actorAttributes = this._actorAttributes; + data.displayModes = CONST.TOKEN_DISPLAY_MODES; + data.dispositions = CONST.TOKEN_DISPOSITIONS; + data.defaultIcons = this.defaultIcons; + data.showCreatureSize = /dnd5e|pf2e/.exec(game.data.system.id) !== null + data.showHP = CONFIG.HP_SUPPORTED; + data.showSystem = CONFIG.SYSTEM_SUPPORTED; + data.languages = CONFIG.LANGUAGES; + data.rollTableList = CONFIG.ROLLTABLES; + data.visionLabel = game.i18n.localize("TOKEN.VisionEnabled") + + Logger.debug(false, "Prepared data", data, CONFIG.ROLLTABLES); + return data; + } + + async _updateObject(event, formData) { + Logger.debug(false, formData); + let min = formData["name.options.min"], + max = formData["name.options.max"]; + if (min < 0) min = 0; + if (max < 0) max = 0; + + if (min > max) { + const tmp = min; + (min = max), (max = tmp); + } + + formData["name.options.min"] = min; + formData["name.options.max"] = max; + + // For name prefix and suffix, if the value is only a space the formData doesn't pick it up, so check and manually set prior to merge. + let prefix = $(this.form).find("input[name='name.number.prefix']").val(); + let suffix = $(this.form).find("input[name='name.number.suffix']").val(); + formData["name.number.prefix"] = formData["name.number.prefix"] !== prefix ? prefix : formData["name.number.prefix"]; + formData["name.number.suffix"] = formData["name.number.suffix"] !== suffix ? suffix : formData["name.number.suffix"]; + + let currentConfig = this.data.CONFIGURATIONS.find(c => c.id === this.#ruleConfig.id); + currentConfig = mergeObject(this.#ruleConfig, formData); + + //this.data.CONFIGURATIONS["Default"] = mergeObject(this.#ruleConfig, formData); + + // if (this._resetOptions === true) { + // // this.object.data.name.options = this.object.dndDefaultNameOptions; + + // const dndOptions = this.dndDefaultNameOptions; + // this.name.options.default = dndOptions.default; + // this.data.name.options.attributes = dndOptions.attributes; + // this._resetOptions = false; + + // this.render(); + // } + + this.render(); + TokenMold.SaveSettings(); + } + + async _onSubmit(ev) { + const attrGroups = $(this.form).find(".attributes"); + let attrs = []; + attrGroups.each((idx, e) => { + const el = $(e); + const icon = el.find(".icon").val(), + value = el.find(".value").val(); + if (icon !== "" && value !== "") + attrs.push({ + icon: icon, + path: value, + }); + }); + this.#ruleConfig.overlay.attrs = attrs; + + this.#ruleConfig.name.options.default = this.form + .querySelector(".default-group") + ?.querySelector(".language")?.value; + const attributes = []; + + const langAttrGroups = this.form.querySelectorAll(".attribute-selection"); + + langAttrGroups.forEach((el) => { + let ret = { languages: {} }; + ret.attribute = el.querySelector(".attribute").value; + el.querySelectorAll(".language-group")?.forEach((langGroup) => { + ret.languages[langGroup.querySelector(".value").value.toLowerCase()] = + langGroup.querySelector(".language").value; + }); + attributes.push(ret); + }); + + this.#ruleConfig.name.options.attributes = attributes; + super._onSubmit(ev); + } + + async #handleAction(event) { + const clickedElement = $(event.currentTarget); + const action = clickedElement.data().action; + let index = clickedElement.data()?.index; + + switch (action) { + case "add-attribute": + this.#ruleConfig.name.options.attributes.push({attribute: "", languages: { "": "random"}}); + break; + case "delete-attribute": + this.#ruleConfig.name.options.attributes.splice(index, 1); + break; + default: + break; + } + + this.render(true); + } + + static get defaultAttrs() { + if (/dnd5e|sw5e/.exec(game.data.system.id) !== null) { + return [ + { + value: "data.attributes.ac.value", + label: "Armor Class", + icon: '', + }, + { + value: "data.skills.prc.passive", + label: "Passive Perception", + icon: '', + }, + ]; + } else + return [ + { + icon: '', + value: "", + }, + ]; + } + + get defaultIcons() { + return [ + "", // eye + " ", //fas fa-shield-alt">', + " ", //fas fa-dice-d20">', + " ", //fas fa-heartbeat">', + " ", //fas fa-hat-wizard">', + " ", //fas fa-shoe-prints">', + " ", //fas fa-walking">', + " ", //fas fa-running">', + " ", //fas fa-coins">', + " ", //fas fa-poop">', + " ", //fas fa-shopping-bag">', + "", //fas fa-money-bill-wave">', + "", // fas fa-suitcase">', + "", //fas fa-fire">', + "", //fas fa-paw">', + "", //fas fa-carrot">', + "", //fas fa-bone">', + "", //fas fa-drumstick-bite">', + "", //fas fa-apple-alt">', + "", //fas fa-fist-raised">', + "", //fas fa-jedi">', + "", //fas fa-meteor">', + "", //fas fa-moon">', + "", //fas fa-rocket">' + "", // brain + "", // child + ]; + } + + get languages() { + return this.object.languages; + } + get _actorAttributes() { let getAttributes = function (data, parent) { parent = parent || []; @@ -398,18 +420,32 @@ export class TokenMoldConfigurationDialog extends HelpFormApplication { return groups; } - #handleAction(event) { - const clickedElement = $(event.currentTarget); - const action = clickedElement.data().action; - - switch (action) { - case "change-config-tab": - this.activeConfig = clickedElement.data().key; - break; - default: - break; - } - - this.render(true); - } + // async _onDragStart(event) { + // const key = $(event.target).closest("[data-key]").data().key; + // Logger.debug(false, "Drag Start", event, key); + + // let transferData = { + // key + // }; + + // event.dataTransfer.setData("text/plain", JSON.stringify(transferData)); + // } + + // async _onDragOver(event) { + // const key = $(event.target).closest("[data-key]").data().key; + // Logger.debug(false, "Drag Over", event, key); + + // } + + // async _onDrop(event) { + // let data; + // try { + // data = JSON.parse(event.dataTransfer.getData("text/plain")); + // } catch (err) { + // return false; + // } + + // const nearestKey = $(event.target).closest("[data-key]")?.data()?.key; + // Logger.debug(false, "Nearest Key:", nearestKey); + // } } \ No newline at end of file diff --git a/module/models/token-mold-mappings.mjs b/module/models/token-mold-mappings.mjs new file mode 100644 index 0000000..40757ca --- /dev/null +++ b/module/models/token-mold-mappings.mjs @@ -0,0 +1,3 @@ +export class TokenMoldMappings { + +} \ No newline at end of file diff --git a/module/utils/token-mold-config.mjs b/module/models/token-mold-rule-config.mjs similarity index 58% rename from module/utils/token-mold-config.mjs rename to module/models/token-mold-rule-config.mjs index 4525b49..e2c2fd3 100644 --- a/module/utils/token-mold-config.mjs +++ b/module/models/token-mold-rule-config.mjs @@ -1,33 +1,21 @@ import { TokenMoldConfigurationDialog } from "../forms/token-mold-configuration-dialog.mjs"; -export class TokenMoldConfig { - #key = ""; - #isLocked = true; +export class TokenMoldRuleConfig { + id = null; - localizeKey = true; - active = false; - config = { - prototypeTokenLinked: true, - disposition: null, - name: null, - custom: null - }; - - unlinkedOnly = true; name = { - use: true, + adjective: { + use: false, + position: "front", + table: "Compendium.token-mold.adjectives.BGNM2VPUyFfA5ZMJ" // English + }, number: { use: true, - prefix: " (", + prefix: "(", suffix: ")", type: "ar", }, remove: false, - prefix: { - use: true, - position: "front", - table: "Compendium.token-mold.adjectives.BGNM2VPUyFfA5ZMJ", // English - }, replace: "", options: { default: "random", @@ -42,16 +30,29 @@ export class TokenMoldConfig { min: 3, max: 9, }, + prefix: { + addCustomWord: false, + addAdjective: true, + customWord: "The", + table: "Compendium.token-mold.adjectives.BGNM2VPUyFfA5ZMJ" // English + }, + suffix: { + addCustomWord: false, + addAdjective: false, + customWord: "the", + table: "Compendium.token-mold.adjectives.BGNM2VPUyFfA5ZMJ" // English + }, baseNameOverride: false, }; hp = { use: true, toChat: true, + ruleSet: null }; size = { use: true, }; - config = { + properties = { use: false, vision: { use: false, @@ -87,30 +88,12 @@ export class TokenMoldConfig { }, }; overlay = { - use: true, + use: false, attrs: TokenMoldConfigurationDialog.defaultAttrs, }; - constructor({key = "Unlinked", localize = true, active = true, isLocked = false}) { - this.#key = key; - this.localizeKey = localize; - this.active = active; - this.#isLocked = isLocked; - } - - get key() { - return game.i18n.localize(`TOKEN-MOLD.CONFIG.${this.#key}`); - } - - get locked() { - return this.#isLocked; - } - - cloneFromSettings(settings) { - if (settings.name) { this.name = settings.name; } - if (settings.hp) { this.hp = settings.hp; } - if (settings.size) { this.size = settings.size; } - if (settings.config) { this.config = settings.config; } - if (settings.overlay) { this.overlay = settings.overlay; } + constructor(defaults = {}) { + mergeObject(this, defaults, {insertKeys: false}); + if (!this.id) { this.id = foundry.utils.randomID(); } } -} \ No newline at end of file +} diff --git a/module/models/token-mold-rule.mjs b/module/models/token-mold-rule.mjs new file mode 100644 index 0000000..9704d5d --- /dev/null +++ b/module/models/token-mold-rule.mjs @@ -0,0 +1,26 @@ +export class TokenMoldRule { + priority = -1; //A negative priority means this rule needs to be placed *last* + active = true; //Rules can be disabled, rather than deleting them + + name = 'Token Mold Rule'; + + /* Token Rules*/ + affectLinked = false; + + dispositionRule = { + active: false, + friendly: false, + neutral: false, + hostile: false + } + + name = { + active: false + } + + configID = null; + + constructor(defaults = {}) { + mergeObject(this, defaults, {insertKeys: false}); + } +} \ No newline at end of file diff --git a/module/token-mold.mjs b/module/token-mold.mjs index e176907..691b75b 100644 --- a/module/token-mold.mjs +++ b/module/token-mold.mjs @@ -2,44 +2,45 @@ import { CONFIG } from "./config.mjs"; import { API } from "./utils/api.mjs"; import { TokenMoldConfigurationDialog } from "./forms/token-mold-configuration-dialog.mjs"; import { Logger } from "./logger/logger.mjs"; -import { NameGenerator } from "./utils/namegenerator.mjs"; -import { TokenMoldConfig } from "./utils/token-mold-config.mjs"; +import { TokenMoldNameGenerator } from "./utils/namegenerator.mjs"; +import { TokenMoldRule } from "./models/token-mold-rule.mjs"; +import { TokenMoldRuleConfig } from "./models/token-mold-rule-config.mjs"; export class TokenMold { #section = null; #configForm = null; + #counters = []; static DefaultSettings() { Logger.info(true, "Loading default Settings"); - return { + const defaults = { GLOBAL: { Name: true, HP: true, Config: false, Overlay: true }, - CONFIGURATIONS: { - "Unlinked": new TokenMoldConfig({active: true, isLocked: true}), - "Linked": new TokenMoldConfig({key: "Linked", isLocked: true}), - "Friendly": new TokenMoldConfig({key: "Friendly", isLocked: true}), - "Neutral": new TokenMoldConfig({key: "Neutral", isLocked: true}), - "Hostile": new TokenMoldConfig({key: "Hostile", isLocked: true}) - } + RULES: new Set(), + CONFIGURATIONS: new Set([new TokenMoldRuleConfig()]) }; + + defaults.RULES.add(new TokenMoldRule({name: "Unlinked Tokens", configID: defaults.CONFIGURATIONS.first().id})); + + return defaults; } static async LoadDicts() { // Remove if replace is unset - const replaceActive = false; - for (let key of Object.keys(CONFIG.SETTINGS.CONFIGURATIONS)) { - if (CONFIG.SETTINGS.CONFIGURATIONS[key].name.replace === "replace") { + let replaceActive = false; + for (let c of CONFIG.SETTINGS.CONFIGURATIONS) { + if (c.name.replace === "replace") { replaceActive = true; break; } } - if (!game.user || !game.user.isGM || replaceActive) { + if (!game.user || !game.user.isGM || !replaceActive) { // Useful to free up memory? its "just" up to 17MB... return; } @@ -53,26 +54,40 @@ export class TokenMold { } static async LoadTable() { - for (let key of Object.keys(CONFIG.SETTINGS.CONFIGURATIONS)) { + //TODO: Make preloading of tables a global setting for performance reasons + CONFIG.ADJECTIVES = {}; + for (let config of CONFIG.SETTINGS.CONFIGURATIONS) { let document; - try { - document = await fromUuid(CONFIG.SETTINGS.CONFIGURATIONS[key].name.prefix.table); - } catch (error) { - // Reset if table not found.. - document = await fromUuid(TokenMold.DefaultSettings().CONFIGURATIONS[key].name.prefix.table); - CONFIG.SETTINGS[key].name.prefix.table = TokenMold.DefaultSettings().name.prefix.table; + if (config.name.prefix.addAdjective) { + try { + document = await fromUuid(config.name.prefix.table); + } catch (error) { + // Reset if table not found.. + document = await fromUuid(TokenMold.DefaultSettings().CONFIGURATIONS.first().name.prefix.table); + config.name.prefix.table = TokenMold.DefaultSettings().CONFIGURATIONS.first().name.prefix.table; + } + + CONFIG.ADJECTIVES[config.name.prefix.table] = document; + } + + if (config.name.suffix.addAdjective) { + try { + document = await fromUuid(config.name.suffix.table); + } catch (error) { + // Reset if table not found.. + document = await fromUuid(TokenMold.DefaultSettings().CONFIGURATIONS.first().name.suffix.table); + config.name.suffix.table = TokenMold.DefaultSettings().CONFIGURATIONS.first().name.suffix.table; + } + + CONFIG.ADJECTIVES[config.name.suffix.table] = document; } - //TODO: Review CONFIG.ADJECTIVES - this may need to go away due to restructuring - CONFIG.ADJECTIVES = document; } } static async SaveSettings() { - if (!CONFIG.ADJECTIVES) { //} || CONFIG.ADJECTIVES.uuid !== CONFIG.SETTINGS.name.prefix.table) { - TokenMold.LoadTable(); - } + TokenMold.LoadTable(); //TODO: This warning needs to be in the CONFIG section, not here. /* @@ -83,9 +98,17 @@ export class TokenMold { } */ + Logger.debug(false, "Saving settings:", CONFIG.SETTINGS); + //Sets can't be saved to flags, so convert them to objects for now + if (CONFIG.SETTINGS.RULES instanceof Set) { CONFIG.SETTINGS.RULES = CONFIG.SETTINGS.RULES.toObject(); } + if (CONFIG.SETTINGS.CONFIGURATIONS instanceof Set) { CONFIG.SETTINGS.CONFIGURATIONS = CONFIG.SETTINGS.CONFIGURATIONS.toObject(); } await game.settings.set("Token-Mold", "everyone", CONFIG.SETTINGS); + //Convert them back + if (!(CONFIG.SETTINGS.RULES instanceof Set)) { CONFIG.SETTINGS.RULES = new Set(Object.values(CONFIG.SETTINGS.RULES)); } + if (!(CONFIG.SETTINGS.CONFIGURATIONS instanceof Set)) { CONFIG.SETTINGS.CONFIGURATIONS = new Set(Object.values(CONFIG.SETTINGS.CONFIGURATIONS)); } + TokenMold.LoadDicts(); - Logger.debug(false, "Saving Settings", CONFIG.SETTINGS); + Logger.debug(false, "Saved Settings", CONFIG.SETTINGS); } constructor() {} @@ -99,6 +122,8 @@ export class TokenMold { this.#registerSettings(); this.#loadSettings(); + + return this.preloadHandlebarTemplates(); } async onReady() { @@ -114,7 +139,7 @@ export class TokenMold { // canvas.hud.TokenMold.clear(); // }); - // this._hookPreTokenCreate(); + this.#registerPreTokenCreate(); // this.barAttributes = await this._getBarAttributes(); TokenMold.LoadDicts(); @@ -122,6 +147,14 @@ export class TokenMold { await TokenMold.LoadTable(); } + preloadHandlebarTemplates = async function() { + //Partials + return loadTemplates([ + "modules/token-mold/templates/partials/hp-tab.hbs", + "modules/token-mold/templates/partials/name-tab.hbs" + ]); + } + hookActorDirectory(html) { this.#section = document.createElement("section"); this.#section.classList.add("token-mold"); @@ -156,8 +189,10 @@ export class TokenMold { #loadSettings() { CONFIG.SETTINGS = game.settings.get("Token-Mold", "everyone"); + + Logger.debug(false, "Loading Settings, start are: ", CONFIG.SETTINGS); // Check for old data - if (CONFIG.SETTINGS.config.data !== undefined) { + if (CONFIG.SETTINGS.config?.data !== undefined) { for (let [key, value] of Object.entries(CONFIG.SETTINGS.config.data)) { CONFIG.SETTINGS.config[key] = { use: true, @@ -180,24 +215,37 @@ export class TokenMold { if (!CONFIG.SETTINGS.CONFIGURATIONS) { const settings = TokenMold.DefaultSettings(); - let unlinked = settings.CONFIGURATIONS["Unlinked"]; - unlinked.name = CONFIG.SETTINGS.name; - unlinked.hp = CONFIG.SETTINGS.hp; - unlinked.size = CONFIG.SETTINGS.size; - unlinked.config = CONFIG.SETTINGS.config; - unlinked.overlay = CONFIG.SETTINGS.overlay; + let defaultSettings = settings.CONFIGURATIONS.first(); + + defaultSettings.name = mergeObject(defaultSettings.name, CONFIG.SETTINGS.name); + defaultSettings.name.number.prefix = defaultSettings.name.number.prefix.trim(); + defaultSettings.hp = mergeObject(defaultSettings.hp, CONFIG.SETTINGS.hp); + defaultSettings.size = mergeObject(defaultSettings.size, CONFIG.SETTINGS.size); + defaultSettings.properties = mergeObject(defaultSettings.properties, CONFIG.SETTINGS.config); + defaultSettings.overlay = mergeObject(defaultSettings.overlay, CONFIG.SETTINGS.overlay); if (!CONFIG.SETTINGS.unlinkedOnly) { //Utilize both linked and unlinked - settings.CONFIGURATIONS["Linked"].active = true; - settings.CONFIGURATIONS["Linked"].cloneFromSettings(unlinked); + settings.RULES.add(new TokenMoldRule({name: "Linked Tokens", priority: 1, affectLinked: true, configID: CONFIG.SETTINGS.CONFIGURATIONS.first().id})); + + settings.RULES = new Set([...settings.RULES.values()].sort((a, b) => b.priority - a.priority)); } + //TODO: Delete old settings + CONFIG.SETTINGS = settings; } CONFIG.SETTINGS = mergeObject(TokenMold.DefaultSettings(), CONFIG.SETTINGS); + if (!(CONFIG.SETTINGS.RULES instanceof Set)) { + CONFIG.SETTINGS.RULES = new Set(Object.values(CONFIG.SETTINGS.RULES)); + } + + if (!(CONFIG.SETTINGS.CONFIGURATIONS instanceof Set)) { + CONFIG.SETTINGS.CONFIGURATIONS = new Set(Object.values(CONFIG.SETTINGS.CONFIGURATIONS)); + } + /* if (/dnd5e|sw5e/.exec(TokenMold.GAME_SYSTEM) !== null) { if (CONFIG.SETTINGS.name.options === undefined) { @@ -293,4 +341,72 @@ export class TokenMold { #updateCheckboxes() { } + + #determineRuleForToken(tokenData) { + let foundRule = null; + + for (let value of CONFIG.SETTINGS.RULES) { + foundRule = value; + if (value.affectLinked != tokenData.actorLink) { + foundRule = null; + } + + if (foundRule) { break; } + } + + return foundRule; + } + + #registerPreTokenCreate() { + Hooks.on("preCreateToken", (token, data, options, userId) => { + const scene = token.parent; + this.#setTokenData(scene, data); + Logger.debug(false, "preCreateToken", token, data); + token.updateSource(data); + }); + } + + #setTokenData(scene, data) { + const actor = game.actors.get(data.actorId); + + let rule = this.#determineRuleForToken(data); + + if (!rule) { return {}; } //No rule, return blank update data - fixes compatibility with other modules and certain systems! + const config = CONFIG.SETTINGS.CONFIGURATIONS.find(c => c.id === rule.configID); + Logger.debug(false, "Found Rule: ", rule, config); + + if (!config) { + Logger.error(true, `Unable to find config for rule!`, rule); + return {}; + } + + if (CONFIG.COUNTERS[scene.id] === undefined) { CONFIG.COUNTERS[scene.id] = {}; } + + if (config.name.use) { + const newName = TokenMoldNameGenerator.GenerateNameFromRuleConfig(actor, config, scene.id, event.getModifierState("Shift"));// this.#modifyName(data, actor, scene.id); + data.name = newName; + setProperty(data, "actorData.name", newName); + } + + //Apply config to token data + + + // Do this for all tokens, even player created ones + // if (this.data.size.use && /dnd5e|pf2e/.exec(TokenMold.GAME_SYSTEM) !== null) + // this._setCreatureSize(data, actor, scene.id); + + // if (this.data.name.use) { + // const newName = this._modifyName(data, actor, scene.id); + // data.name = newName; + // setProperty(data, "actorData.name", newName); + // } + + // if (/dnd5e|dcc/.exec(TokenMold.GAME_SYSTEM) !== null) { + // if (this.data.hp.use) this._rollHP(data, actor); + // } + + // if (this.data.config.use) this._overwriteConfig(data, actor); + + return data; + } } \ No newline at end of file diff --git a/module/utils/namegenerator.mjs b/module/utils/namegenerator.mjs index d611ed2..eaab82c 100644 --- a/module/utils/namegenerator.mjs +++ b/module/utils/namegenerator.mjs @@ -1,4 +1,280 @@ -export class NameGenerator { +import { Logger } from "../logger/logger.mjs"; +import { CONFIG } from "../config.mjs"; +import { TokenMoldRuleConfig } from "../models/token-mold-rule-config.mjs"; + +export class TokenMoldNameGenerator { + static GenerateNameFromRuleConfig(actor, ruleConfig, sceneID, shiftOverride) { + Logger.debug(false, "Generating Name", actor, ruleConfig, shiftOverride, CONFIG); + + const nameConfig = ruleConfig.name; + let name = actor.prototypeToken.name; + + if (["remove", "replace"].includes(nameConfig.replace) && !(nameConfig.baseNameOverride && shiftOverride)) { + name = ""; + } + + if (nameConfig.replace === "replace") { + name = TokenMoldNameGenerator.#GenerateNewName(actor, ruleConfig); + } + + let namePrefix = ""; + if (nameConfig.prefix.addCustomWord) { namePrefix = nameConfig.prefix.customWord; } + if (nameConfig.prefix.addAdjective) { namePrefix += ` ${TokenMoldNameGenerator.#GetAdjective(nameConfig.prefix.table)}`; } + + if (namePrefix.length > 0) { name = `${namePrefix} ${name}`.trim(); } + + if (nameConfig.suffix.addCustomWord) { name += ` ${nameConfig.suffix.customWord}`; } + if (nameConfig.suffix.addAdjective) { name += ` ${TokenMoldNameGenerator.#GetAdjective(nameConfig.suffix.table)}`; } + if (nameConfig.number.use) { + let numberSuffix = ""; + let number = 0; + + // Check if number in session database + if (CONFIG.COUNTERS[sceneID][actor.id] !== undefined) { + number = CONFIG.COUNTERS[sceneID][actor.id]; + } else { + // Extract number from last created token with the same actor ID + const sameTokens = game.scenes.get(sceneID).tokens.filter((e) => e.actorId === actor.id); + if (sameTokens.length !== 0) { + const lastTokenName = sameTokens[sameTokens.length - 1].name; + // Split by prefix and take last element + let tmp = lastTokenName.split(nameConfig.number.prefix).pop(); + if (tmp !== "") { + // Split by suffix and take first element + number = tmp.split(nameConfig.number.suffix)[0]; + } + } + } + + // Convert String back to number + switch (nameConfig.number.type) { + case "ar": + number = parseInt(number); + break; + case "alu": + number = TokenMoldNameGenerator.#Dealphabetize(number.toString(), "upper"); + break; + case "all": + number = TokenMoldNameGenerator.#Dealphabetize(number.toString(), "lower"); + break; + case "ro": + number = TokenMoldNameGenerator.#Deromanize(number); + break; + } + + // If result is no number, set to zero + if (isNaN(number)) { + number = 0; + } else { + // count upwards + if (nameConfig.number.range > 1) { + number += Math.ceil(Math.random() * nameConfig.number.range); + } else { + number++; + } + } + + switch (nameConfig.number.type) { + case "alu": + number = TokenMoldNameGenerator.#Alphabetize(number, "upper"); + break; + case "all": + number = TokenMoldNameGenerator.#Alphabetize(number, "lower"); + break; + case "ro": + number = TokenMoldNameGenerator.#Romanize(number); + break; + } + + CONFIG.COUNTERS[sceneID][actor.id] = number; + + numberSuffix = nameConfig.number.prefix + number + nameConfig.number.suffix; + name += ` ${numberSuffix}`; + name = name.trim(); + } + + return name; + } + + static #GetAdjective(tableName) { + const adj = CONFIG.ADJECTIVES[tableName].results._source[Math.floor(CONFIG.ADJECTIVES[tableName].results.size * Math.random())].text; + return adj; + } + + /** + * Thanks for 'trdischat' for providing this awesome name generation algorithm! + * Base idea: + * - Choose a language (depending on settings chosen) + * - Choose a random starting trigram for the language, weighted by frequency used + * - Go on choosing letters like before, using the previous found letter as starting letter of the trigram, until maximum is reached + * @param {*} actor + * @param {TokenMoldRuleConfig} ruleConfig + */ + static #GenerateNewName(actor, ruleConfig) { + const attributes = ruleConfig.name.options.attributes || []; + + let lang; + for (let attribute of attributes) { + const langs = attribute.languages; + const val = TString(getProperty(actor.system, attribute.attribute)).toLowerCase(); + + lang = langs[val]; + + if (lang !== undefined) break; + } + + if (lang === undefined) lang = ruleConfig.name.options.default; + + if (lang === "random") { + const keys = Object.keys(CONFIG.DICTIONARY); + lang = keys[Math.floor(Math.random() * keys.length)]; + } + + const minNameLen = ruleConfig.name.options.min || 6; + const maxNameLen = ruleConfig.name.options.max || 9; + + const nameLength = Math.floor(Math.random() * (maxNameLen - minNameLen + 1)) + minNameLen; + let newName = TokenMoldNameGenerator.#ChooseWeighted(CONFIG.DICTIONARY[lang].beg); + const ltrs = (x, y, b) => x in b && y in b[x] && Object.keys(b[x][y]).length > 0 ? b[x][y] : false; + + for (let i = 4; i <= nameLength; i++) { + const c1 = newName.slice(-2, -1); + const c2 = newName.slice(-1); + const br = i == nameLength ? CONFIG.DICTIONARY[lang].end : CONFIG.DICTIONARY[lang].mid; + const c3 = ltrs(c1, c2, br) || ltrs(c1, c2, CONFIG.DICTIONARY[lang].all) || {}; + if (c1 == c2 && c1 in c3) delete c3[c1]; + if (Object.keys(c3).length == 0) break; + newName = newName + TokenMoldNameGenerator.#ChooseWeighted(c3); + } + + newName = newName[0] + TokenMoldNameGenerator.#ChangeCase(newName.slice(1), CONFIG.DICTIONARY[lang].upper, CONFIG.DICTIONARY[lang].lower); + return newName; + } + + static #ChooseWeighted(items) { + var keys = Object.keys(items); + var vals = Object.values(items); + var sum = vals.reduce((accum, elem) => accum + elem, 0); + var accum = 0; + vals = vals.map((elem) => (accum = elem + accum)); + var rand = Math.random() * sum; + return keys[vals.filter((elem) => elem <= rand).length]; + } + + static #ChangeCase(txt, fromCase, toCase) { + var res = ""; + var c = ""; + for (c of txt) { + let loc = fromCase.indexOf(c); + if (loc < 0) { + res = res + c; + } else { + res = res + toCase[loc]; + } + } + return res; + } + + static #Dealphabetize(num, letterStyle) { + if (num === "0") return 0; + let ret = 0; + const startValue = { + upper: 64, + lower: 96, + }[letterStyle]; + + for (const char of num) ret += char.charCodeAt(0) - startValue; + + return ret; + } + + static #Alphabetize(num, letterStyle) { + let ret = ""; + + const startValue = { + upper: 64, + lower: 96, + }[letterStyle]; + + while (num >= 26) { + ret += String.fromCharCode(startValue + 26); + num -= 26; + } + + ret += String.fromCharCode(startValue + num); + + return ret; + } + + // Romanizes a number, code is from : http://blog.stevenlevithan.com/archives/javascript-roman-numeral-converter + static #Romanize(num) { + if (!+num) return false; + var digits = String(+num).split(""), + key = [ + "", + "C", + "CC", + "CCC", + "CD", + "D", + "DC", + "DCC", + "DCCC", + "CM", + "", + "X", + "XX", + "XXX", + "XL", + "L", + "LX", + "LXX", + "LXXX", + "XC", + "", + "I", + "II", + "III", + "IV", + "V", + "VI", + "VII", + "VIII", + "IX", + ], + roman = "", + i = 3; + while (i--) roman = (key[+digits.pop() + i * 10] || "") + roman; + return Array(+digits.join("") + 1).join("M") + roman; + } + + // code is from : http://blog.stevenlevithan.com/archives/javascript-roman-numeral-converter + static #Deromanize(rom) { + if (typeof rom !== "string") return 0; + let str = rom.toUpperCase(), + validator = /^M*(?:D?C{0,3}|C[MD])(?:L?X{0,3}|X[CL])(?:V?I{0,3}|I[XV])$/, + token = /[MDLV]|C[MD]?|X[CL]?|I[XV]?/g, + key = { + M: 1000, + CM: 900, + D: 500, + CD: 400, + C: 100, + XC: 90, + L: 50, + XL: 40, + X: 10, + IX: 9, + V: 5, + IV: 4, + I: 1, + }, + num = 0, + m; + if (!(str && validator.test(str))) return false; + while ((m = token.exec(str))) num += key[m[0]]; + return num; + } //TODO - Update this to be able to be setup manually static dndDefaultNameOptions() { diff --git a/styles/token-mold.css b/styles/token-mold.css index ccfd4ea..e04fa6b 100644 --- a/styles/token-mold.css +++ b/styles/token-mold.css @@ -62,6 +62,9 @@ .token-mold nav .item { flex: 1 auto; } +.token-mold .tmold_custom_word_label { + flex: 8; +} .token-mold ul.token-mold-config-tabs { list-style-type: none; margin: 0px; @@ -94,7 +97,7 @@ .token-mold ul.token-mold-config-tabs li:hover:last-child { padding: 2px 6px; } -.token-mold ul.token-mold-config-tabs li.token-mod-active-config { +.token-mold ul.token-mold-config-tabs li.token-mold-active-config { color: black; text-shadow: 0 0 8px var(--color-shadow-primary); } diff --git a/templates/config.hbs b/templates/config.hbs index 7bfa9e7..d2e0bfb 100644 --- a/templates/config.hbs +++ b/templates/config.hbs @@ -2,33 +2,15 @@
-
-
-
- This is where the rule is configured. +
+
+ {{> "modules/token-mold/templates/partials/name-tab.hbs"}}
-
-
- -
-
- -
-
- -
-
- +
+ {{> "modules/token-mold/templates/partials/hp-tab.hbs"}}