Add saving and editing of default attributes and their values (closes #29)

This commit is contained in:
Oliver 2026-03-02 21:48:59 -07:00
parent 5159db3d33
commit d540cc72f6
6 changed files with 178 additions and 34 deletions

View file

@ -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 };

View file

@ -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;

View file

@ -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);
};
});

View file

@ -1,3 +1,4 @@
import "./api.mjs";
import "./hooks/init.mjs";
import "./hooks/userConnected.mjs";
import "./hooks/renderSettingsConfig.mjs";

View file

@ -59,4 +59,10 @@ export function registerWorldSettings() {
}),
scope: `world`,
});
game.settings.register(__ID__, `actorDefaultAttributes`, {
config: false,
type: Object,
scope: `world`,
});
};