From d540cc72f69db6079984dabae8e0b16582bb1930 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 2 Mar 2026 21:48:59 -0700 Subject: [PATCH] Add saving and editing of default attributes and their values (closes #29) --- langs/en-ca.json | 11 +- module/apps/AttributeManager.mjs | 138 ++++++++++++++++++++------ module/documents/Actor.mjs | 18 ++++ module/hooks/renderSettingsConfig.mjs | 38 +++++++ module/main.mjs | 1 + module/settings/world.mjs | 6 ++ 6 files changed, 178 insertions(+), 34 deletions(-) create mode 100644 module/hooks/renderSettingsConfig.mjs diff --git a/langs/en-ca.json b/langs/en-ca.json index cf664c7..6dee3ec 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -30,6 +30,11 @@ "false": "Not Resizable", "true": "Resizable" } + }, + "actorDefaultAttributes": { + "name": "Remove Default Attributes", + "hint": "This removes the default attributes that are applied when a new actor is created, making it so that no attributes get created alongside the actor.", + "label": "Remove Attributes" } }, "sheet-names": { @@ -59,7 +64,8 @@ "has-max": "Has Maximum?", "name-placeholder": "Attribute Name...", "no-attributes": "No attributes yet", - "add-new-attribute": "Add New Attribute" + "add-new-attribute": "Add New Attribute", + "default-attribute-values": "Default Attribute Values" }, "PlayerSheet": { "manage-attributes": "Manage Attributes", @@ -100,6 +106,9 @@ "invalid-socket": "Invalid socket data received, this means a module or system bug is present.", "unknown-socket-event": "An unknown socket event was received: {event}", "malformed-socket-payload": "Socket event \"{event}\" received with malformed payload. Details: {details}" + }, + "success": { + "saved-default-attributes": "Successfully saved default Actor attributes" } } } diff --git a/module/apps/AttributeManager.mjs b/module/apps/AttributeManager.mjs index 9830baf..ac94e19 100644 --- a/module/apps/AttributeManager.mjs +++ b/module/apps/AttributeManager.mjs @@ -1,5 +1,6 @@ import { __ID__, filePath } from "../consts.mjs"; import { attributeSorter } from "../utils/attributeSort.mjs"; +import { ask } from "../utils/DialogManager.mjs"; import { localizer } from "../utils/localizer.mjs"; import { toID } from "../utils/toID.mjs"; @@ -22,6 +23,14 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) }, window: { resizable: true, + controls: [ + { + icon: `fa-solid fa-globe`, + label: `Save As Defaults`, + visible: () => game.user.isGM, + action: `saveAsDefault`, + } + ], }, form: { submitOnChange: false, @@ -31,6 +40,7 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) actions: { addNew: this.#addNew, removeAttribute: this.#remove, + saveAsDefault: this.#saveAsDefaults, }, }; @@ -73,16 +83,39 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) new DragDrop.implementation({ dragSelector: `.attribute-drag-handle`, dropSelector: `.attributes`, - permissions: { - dragstart: this._canDragStart.bind(this), - drop: this._canDragDrop.bind(this), - }, callbacks: { dragstart: this._onDragStart.bind(this), drop: this._onDrop.bind(this), }, }).bind(this.element); }; + + /** @this {AttributeManager} */ + static async #onSubmit() { + const entries = Object.entries(this.#attributes) + .map(([id, attr]) => { + if (attr == null) { + return [ id, attr ]; + }; + + if (attr.isNew) { + delete attr.isNew; + return [ toID(attr.name), attr ]; + }; + + return [ id, attr ]; + }); + const data = Object.fromEntries(entries); + this.#attributes = data; + + const diff = diffObject( + this.#doc.system.attr, + data, + { inner: false, deletionKeys: true }, + ); + + await this.#doc.update({ "system.attr": diff }); + }; // #endregion Lifecycle // #region Data Prep @@ -152,48 +185,87 @@ export class AttributeManager extends HandlebarsApplicationMixin(ApplicationV2) static async #remove($e, element) { const attribute = element.closest(`[data-attribute]`)?.dataset.attribute; if (!attribute) { return }; - delete this.#attributes[attribute]; - this.#attributes[`-=${attribute}`] = null; + if (game.release.generation < 14) { + delete this.#attributes[attribute]; + this.#attributes[`-=${attribute}`] = null; + } + else { + this.#attributes[attribute] = _del; + } await this.render({ parts: [ `attributes` ] }); }; /** @this {AttributeManager} */ - static async #onSubmit() { - const entries = Object.entries(this.#attributes) - .map(([id, attr]) => { - if (attr == null) { - return [ id, attr ]; - }; + static async #saveAsDefaults() { + const attrs = deepClone(this.#attributes); - if (attr.isNew) { - delete attr.isNew; - return [ toID(attr.name), attr ]; - }; + // Prompt the user for what values they want to save the attributes with + const inputs = []; + for (const attr of Object.values(attrs)) { + const id = toID(attr.name); - return [ id, attr ]; + if (attr.isRange) { + inputs.push( + { + type: `collapse`, + summary: attr.name, + inputs: [ + { + key: `${id}.value`, + type: `input`, + inputType: `number`, + label: `Value`, + defaultValue: attr.value, + }, + { + key: `${id}.max`, + type: `input`, + inputType: `number`, + label: `Maximum`, + defaultValue: attr.max, + }, + ], + }, + { type: `divider` } + ); + continue; + }; + + inputs.push({ + key: `${id}.value`, + type: `input`, + inputType: `number`, + label: `${attr.name}`, + defaultValue: attr.value, }); - const data = Object.fromEntries(entries); - this.#attributes = data; - const diff = diffObject( - this.#doc.system.attr, - data, - { inner: false, deletionKeys: true }, - ); + inputs.push({ type: `divider` }); + }; - await this.#doc.update({ "system.attr": diff }); + const prompt = { + id: `${this.#doc.id}-global-attr-saving`, + inputs: inputs.slice(0, -1), + alwaysUseAnswerObject: true, + window: { title: `taf.Apps.AttributeManager.default-attribute-values` }, + }; + + const response = await ask(prompt); + switch (response.state) { + case `errored`: + ui.notifications.error(response.error); + case `fronted`: + return; + }; + + if (!response.answers) { return }; + + const fullAttrs = mergeObject(attrs, response.answers); + game.settings.set(__ID__, `actorDefaultAttributes`, fullAttrs); + ui.notifications.success(`taf.notifs.success.saved-default-attributes`); }; // #endregion Actions // #region Drag & Drop - _canDragStart() { - return this.#doc.isOwner; - }; - - _canDragDrop() { - return this.#doc.isOwner; - }; - _onDragStart(event) { const target = event.currentTarget.closest(`[data-attribute]`); if (`link` in event.target.dataset) { return }; diff --git a/module/documents/Actor.mjs b/module/documents/Actor.mjs index ff72c77..91c2bfa 100644 --- a/module/documents/Actor.mjs +++ b/module/documents/Actor.mjs @@ -1,6 +1,24 @@ +import { __ID__ } from "../consts.mjs"; + const { Actor } = foundry.documents; +const { hasProperty } = foundry.utils; export class TAFActor extends Actor { + + // #region Lifecycle + async _preCreate(data, options, user) { + + // Assign the defaults from the world setting if they exist + const defaults = game.settings.get(__ID__, `actorDefaultAttributes`) ?? {}; + if (!hasProperty(data, `system.attr`)) { + const value = game.release.generation > 13 ? _replace(defaults) : defaults; + this.updateSource({ "system.==attr": value }); + }; + + return super._preCreate(data, options, user); + }; + // #endregion Lifecycle + async modifyTokenAttribute(attribute, value, isDelta = false, isBar = true) { const attr = foundry.utils.getProperty(this.system, attribute); const current = isBar ? attr.value : attr; diff --git a/module/hooks/renderSettingsConfig.mjs b/module/hooks/renderSettingsConfig.mjs new file mode 100644 index 0000000..62eb341 --- /dev/null +++ b/module/hooks/renderSettingsConfig.mjs @@ -0,0 +1,38 @@ +import { __ID__ } from "../consts.mjs"; + +Hooks.on(`renderSettingsConfig`, (app, html, context, options) => { + /* + This section is used to insert a button into the settings config that unsets + a world setting when it exists but doesn't allow any other form of editing it. + */ + if (game.user.isGM && game.settings.get(__ID__, `actorDefaultAttributes`)) { + const formGroup = document.createElement(`div`); + formGroup.classList = `form-group`; + + const label = document.createElement(`div`); + label.innerHTML = _loc(`taf.settings.actorDefaultAttributes.name`); + + const formFields = document.createElement(`div`); + formFields.classList = `form-fields`; + + const button = document.createElement(`button`); + button.type = `button`; + button.innerHTML = _loc(`taf.settings.actorDefaultAttributes.label`); + button.addEventListener(`click`, () => { + game.settings.set(__ID__, `actorDefaultAttributes`, undefined); + }); + + const hint = document.createElement(`p`); + hint.classList = `hint`; + hint.innerHTML = _loc(`taf.settings.actorDefaultAttributes.hint`); + + formFields.appendChild(button); + formGroup.appendChild(label); + formGroup.appendChild(formFields); + formGroup.appendChild(hint); + + /** @type {HTMLElement|undefined} */ + const tab = html.querySelector(`.tab[data-group="categories"][data-tab="system"]`); + tab?.insertAdjacentElement(`afterbegin`, formGroup); + }; +}); diff --git a/module/main.mjs b/module/main.mjs index 05cf7d9..df1c170 100644 --- a/module/main.mjs +++ b/module/main.mjs @@ -1,3 +1,4 @@ import "./api.mjs"; import "./hooks/init.mjs"; import "./hooks/userConnected.mjs"; +import "./hooks/renderSettingsConfig.mjs"; diff --git a/module/settings/world.mjs b/module/settings/world.mjs index a351c37..330f433 100644 --- a/module/settings/world.mjs +++ b/module/settings/world.mjs @@ -59,4 +59,10 @@ export function registerWorldSettings() { }), scope: `world`, }); + + game.settings.register(__ID__, `actorDefaultAttributes`, { + config: false, + type: Object, + scope: `world`, + }); };