`
- let presetSelector = new Dialog({
- title: "Preset Selector",
+ let presetSelector = new foundry.applications.api.DialogV2({
+ window: {
+ title: "Preset Selector"
+ },
content: `
-
-
- ${content}
-
`,
- content,
- buttons: {
- one: {
- label: "Update",
- icon: ``,
- callback: (html) => {
- let updatePreset = html.find("[name=presets]")[0].value;
- let preset = presets.find(p => p.id === updatePreset)
- new PresetConfig(preset).render(true);
- }
- },
- two: {
- label: "Create Copy",
- icon: ``,
- callback: (html) => {
- let updatePreset = html.find("[name=presets]")[0].value;
- let preset = presets.find(p => p.id === updatePreset)
- // copy and remove ID so it's created as new
- preset = deepClone(preset);
- delete preset.id;
- new PresetConfig(preset).render(true);
- }
- },
- three: {
- label: "Delete",
- icon: ``,
- callback: (html) => {
- let updatePreset = html.find("[name=presets]")[0].value;
- let index = presets.findIndex(p => p.id === updatePreset);
- new Dialog({
- title: "Conformation",
- content: `Are you sure you want to remove this preset`,
- buttons: {
- one: {
- label: "Confirm",
- icon: ``,
- callback: () => {
- presets.splice(index, 1);
- game.settings.set("ATL", "presets", presets);
- }
- },
- two: {
- label: "Return",
- icon: ``,
- callback: () => presetSelector.render(true)
- }
- }
- }).render(true)
+
+
+
+
`,
+ buttons: [{
+ action: "update",
+ label: "Update",
+ icon: ``,
+ },
+ {
+ action: "copy",
+ label: "Create Copy",
+ icon: ``,
+ },
+ {
+ action: "delete",
+ label: "Delete",
+ icon: ``,
+ },
+ {
+ action: "new",
+ label: "Add New",
+ icon: ``,
+ }],
+ submit: async result => {
+ if (result === "update") {
+ let updatePreset = document.getElementById("presets").value
+ let preset = presets.find(p => p.id === updatePreset)
+
+ new PresetConfig({}, preset).render(true);
+ }
+ else if (result === "copy") {
+ let updatePreset = document.getElementById("presets").value
+ let preset = presets.find(p => p.id === updatePreset)
+ preset = deepClone(preset);
+ delete preset.id;
+ new PresetConfig({}, preset).render(true);
+ }
+ else if (result === "delete") {
+ let preset = document.getElementById("presets").value
+ let index = presets.findIndex(p => p.id === preset);
+ const proceed = await foundry.applications.api.DialogV2.confirm({
+ window: {
+ title: "Conformation"
+ },
+ content: `Are you sure you want to remove this preset`,
+ })
+ if (proceed) {
+ presets.splice(index, 1);
+ game.settings.set("ATL", "presets", presets);
}
- },
- four: {
- label: "Add New",
- icon: ``,
- callback: () => new PresetConfig().render(true)
+ //else presetSelector.render(true);
+ }
+ else if (result === "new") {
+ new PresetConfig().render(true);
}
}
});
@@ -340,6 +347,7 @@ class ATL {
console.log("ATE | apply preset", change.value, preset);
Object.entries(preset)
.forEach(([key, value]) => {
+ if (key === "tokenName") key = "name" //workaround for DAE
const originalValue = getProperty(originals, key);
applyOverride(key, value, originalValue);
});
diff --git a/src/preset-config.js b/src/preset-config.js
index c2a9f65..37b6bf5 100644
--- a/src/preset-config.js
+++ b/src/preset-config.js
@@ -1,20 +1,21 @@
+const { HandlebarsApplicationMixin, ApplicationV2, DocumentSheetV2, Base } = foundry.applications.api;
/**
* The Application used for defining a preset configuration that can be used by the `ATL.preset`
* active effect key. It can handle updating an existing preset as well as creating a new one.
*/
-export class PresetConfig extends FormApplication {
+export class PresetConfig extends HandlebarsApplicationMixin(ApplicationV2) {
/**
- * Create a new application to add/edit a preset.
+ * Create a new application to add/edit a preset.
+ * @param {ApplicationConfiguration} options Options used to configure the Application instance
* @param {Object} object The ATL preset, or `undefined` if creating a new one from scratch
- * @param {FormApplicationOptions} options Application configuration options
*/
- constructor(object = {}, options = {}) {
- super(object, options);
+ constructor(options, object = {}) {
+ super(options);
/**
* The token change preset
*/
- this.preset = this.object;
+ this.preset = object;
/**
* Whether this app is creating a new preset or not
@@ -27,29 +28,183 @@ export class PresetConfig extends FormApplication {
this.fieldsChanged = [];
}
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ["sheet", "preset-config"],
+ /** @inheritDoc */
+ static DEFAULT_OPTIONS = {
+ classes: ["preset-config"],
+ tag: "form",
+ position: {
+ width: 560
+ },
+ window: {
title: "ATL Light Editor",
- template: "modules/ATL/templates/preset-config.hbs",
- width: 480,
- height: "auto",
+ icon: "fas fa-plus-circle",
+ contentClasses: ["standard-form"],
+ },
+ form: {
+ handler: PresetConfig.#onSubmit,
+ //submitOnChange: false,
+ closeOnSubmit: true
+ },
+ actions: {
+ addDetectionMode: PresetConfig.#onAddDetectionMode,
+ removeDetectionMode: PresetConfig.#onRemoveDetectionMode
+ }
+ }
+
+ /** @override */
+ static PARTS = {
+ header: {
+ template: "modules/ATL/templates/header.hbs",
+ },
+ tabs: {
+ template: "templates/generic/tab-navigation.hbs"
+ },
+ appearance: {
+ template: "modules/ATL/templates/appearance.hbs", scrollable: [""]
+ },
+ identity: {
+ template: "modules/ATL/templates/identity.hbs", scrollable: [""]
+ },
+ vision: {
+ template: "modules/ATL/templates/vision.hbs", scrollable: [""]
+ },
+ light: {
+ template: "modules/ATL/templates/light.hbs", scrollable: [""]
+ },
+ //resources: {
+ // template: "modules/ATL/templates/resources.hbs", scrollable: [""]
+ //},
+ footer: {
+ template: "templates/generic/form-footer.hbs",
+ },
+ };
+
+ /** @override */
+ static TABS = {
+ sheet: {
tabs: [
- {
- navSelector: '.tabs[data-group="main"]',
- contentSelector: "form",
- initial: "appearance",
- },
- {
- navSelector: '.tabs[data-group="light"]',
- contentSelector: '.tab[data-tab="light"]',
- initial: "basic",
- },
+ { id: "identity", icon: "fa-solid fa-memo-pad" },
+ { id: "appearance", icon: "fa-solid fa-square-user" },
+ { id: "vision", icon: "fa-solid fa-eye" },
+ { id: "light", icon: "fa-solid fa-lightbulb" }
+ //{ id: "resources", icon: "fa-solid fa-heart" }
],
- closeOnSubmit: true,
- });
+ initial: "appearance",
+ labelPrefix: "TOKEN.TABS"
+ }
+ };
+
+ /**
+ * Localized Token Display Modes
+ * @returns {Record}
+ */
+ static get DISPLAY_MODES() {
+ PresetConfig.#DISPLAY_MODES ??= Object.entries(CONST.TOKEN_DISPLAY_MODES).reduce((modes, [key, value]) => {
+ modes[value] = game.i18n.localize(`TOKEN.DISPLAY_${key}`);
+ return modes;
+ }, {});
+ return PresetConfig.#DISPLAY_MODES;
+ }
+
+ static #DISPLAY_MODES;
+
+ /**
+ * Localized Token Dispositions
+ * @returns {Record}
+ */
+ static get TOKEN_DISPOSITIONS() {
+ PresetConfig.#TOKEN_DISPOSITIONS ??= Object.entries(CONST.TOKEN_DISPOSITIONS)
+ .reduce((dispositions, [key, value]) => {
+ dispositions[value] = game.i18n.localize(`TOKEN.DISPOSITION.${key}`);
+ return dispositions;
+ }, {});
+ return PresetConfig.#TOKEN_DISPOSITIONS;
+ }
+
+ static #TOKEN_DISPOSITIONS;
+
+ /**
+ * Localized Token Turn Marker modes
+ * @returns {Record}
+ */
+ static get TURN_MARKER_MODES() {
+ PresetConfig.#TURN_MARKER_MODES ??= Object.entries(CONST.TOKEN_TURN_MARKER_MODES)
+ .reduce((modes, [key, value]) => {
+ modes[value] = game.i18n.localize(`TOKEN.TURNMARKER.MODES.${key}`);
+ return modes;
+ }, {});
+ return PresetConfig.#TURN_MARKER_MODES;
+ }
+
+ static #TURN_MARKER_MODES;
+
+ /**
+ * Localized Token Shapes
+ * @returns {Record}
+ */
+ static get TOKEN_SHAPES() {
+ PresetConfig.#TOKEN_SHAPES ??= Object.entries(CONST.TOKEN_SHAPES)
+ .reduce((shapes, [key, value]) => {
+ shapes[value] = game.i18n.localize(`TOKEN.SHAPES.${key}.label`);
+ return shapes;
+ }, {});
+ return PresetConfig.#TOKEN_SHAPES;
+ }
+
+ static #TOKEN_SHAPES;
+ /* -------------------------------------------- */
+ /**
+ * Maintain a copy of the original to show a real-time preview of changes.
+ * @type {TokenDocument|PrototypeToken|null}
+ * @protected
+ */
+ _preview = null;
+
+ /* -------------------------------------------- */
+
+ /**
+ * Process form submission for the sheet
+ * @this {PresetConfig} The handler is called with the application as its bound scope
+ * @param {SubmitEvent} event The originating form submission event
+ * @param {HTMLFormElement} form The form element that was submitted
+ * @param {FormDataExtended} formData Processed data for the submitted form
+ * @returns {Promise}
+ */
+ static async #onSubmit(event, form, formData) {
+ console.log("ATL |", "_updateObject called with formData:", formData.object);
+ // Mirror token scale
+ if ("scale" in formData.object) {
+ formData.object["texture.scaleX"] = formData.object.scale * (formData.object.mirrorX ? -1 : 1);
+ formData.object["texture.scaleY"] = formData.object.scale * (formData.object.mirrorY ? -1 : 1);
+ }
+
+ if (this.fieldsChanged.includes("scale" || "mirrorX" || "mirrorY")) this.fieldsChanged.push("texture.scaleX", "texture.scaleY");
+ for (const key of ["scale", "mirrorX", "mirrorY"]) delete formData.object[key];
+
+ // Set default name if creating a new preset with no name
+ if (this.newMode && !formData.object.name) {
+ const presets = game.settings.get("ATL", "presets");
+ const count = presets?.length;
+ formData.object.name = `New Preset (${count + 1})`;
+ this.fieldsChanged.push("name")
+ }
+
+ // Remove name change if updating a preset and trying to clear the name
+ if (!this.newMode && "name" in formData.object && !formData.object.name) delete formData.object.name;
+
+ // apply the changes to the original preset
+ Object.entries(formData.object)
+ .filter(([k, _]) => this.fieldsChanged.includes(k))
+ .forEach(([k, v]) => {
+ if (v === "" || v === null) this._clearProperty(this.preset, k);
+ else foundry.utils.setProperty(this.preset, k, v);
+ });
+ console.log("updated preset:", this.preset);
+
+ PresetConfig.savePreset(this.preset);
}
+
static savePreset(preset) {
// put all the presets into a collection
const collection = new Collection();
@@ -59,23 +214,28 @@ export class PresetConfig extends FormApplication {
// add or update in collection
if (!preset.id) preset.id = foundry.utils.randomID();
collection.set(preset.id, preset);
-
// save collection
presets = collection.toJSON();
game.settings.set("ATL", "presets", presets);
}
- getData(options) {
- const gridUnits = game.system.gridUnits;
+ /* -------------------------------------------- */
+
+ /** @inheritDoc */
+ async _prepareContext(options) {
+ const context = await super._prepareContext(options);
// prepare Preset data
- const preset = foundry.utils.deepClone(this.object);
+ const preset = foundry.utils.deepClone(this.preset);
- return {
+ return Object.assign(context, {
+ rootId: this.id,
object: preset,
- gridUnits: gridUnits || game.i18n.localize("GridUnits"),
- colorationTechniques: AdaptiveLightingShader.SHADER_TECHNIQUES,
+ gridUnits: game.i18n.localize("GridUnits"),
+ displayModes: PresetConfig.DISPLAY_MODES,
visionModes: Object.values(CONFIG.Canvas.visionModes).filter((f) => f.tokenConfig),
+ detectionModes: Object.values(CONFIG.Canvas.detectionModes).filter(f => f.tokenConfig),
+ preparedDetectionModes: this.preset?.detectionModes,
lightAnimations: Object.entries(CONFIG.Canvas.lightAnimations).reduce(
(obj, e) => {
obj[e[0]] = game.i18n.localize(e[1].label);
@@ -83,59 +243,144 @@ export class PresetConfig extends FormApplication {
},
{ "": game.i18n.localize("None") }
),
- scale: Math.abs(this.object.texture?.scaleX || 1),
- };
+ shapes: PresetConfig.TOKEN_SHAPES,
+ colorationTechniques: foundry.canvas.rendering.shaders.AdaptiveLightingShader.SHADER_TECHNIQUES,
+ scale: (Math.abs(this.preset.texture?.scaleX) || 1),
+ mirrorX: this.preset.texture?.scaleX < 0,
+ mirrorY: this.preset.texture?.scaleY < 0,
+ textureFitModes: CONST.TEXTURE_DATA_FIT_MODES.reduce((obj, fit) => {
+ obj[fit] = game.i18n.localize(`TEXTURE_DATA.FIT.${fit}`);
+ return obj;
+ }, {}),
+ movementActions: Object.entries(CONFIG.Token.movement.actions).reduce(
+ (choices, [action, { label, canSelect }]) => {
+ if (canSelect(this.token)) choices[action] = label;
+ return choices;
+ }, {}),
+ dispositions: PresetConfig.TOKEN_DISPOSITIONS,
+ buttons: [
+ { type: "submit", icon: "fa-solid fa-save", label: "SETTINGS.Save" },
+ ]
+ });
}
+ /* -------------------------------------------- */
- _getSubmitData(updateData = {}) {
- const formData = super._getSubmitData(updateData);
+ /** @inheritDoc */
+ async _preFirstRender(context, options) {
+ await super._preFirstRender(context, options);
+ }
- // Mirror token scale
- if ("scale" in formData) {
- formData["texture.scaleX"] = formData.scale * (formData.mirrorX ? -1 : 1);
- formData["texture.scaleY"] = formData.scale * (formData.mirrorY ? -1 : 1);
- }
- ["scale", "mirrorX", "mirrorY"].forEach((k) => delete formData[k]);
- if (this.fieldsChanged.includes("scale")) this.fieldsChanged.push("texture.scaleX", "texture.scaleY");
+ /* -------------------------------------------- */
- // Set default name if creating a new preset with no name
- if (this.newMode && !formData.name) {
- const presets = game.settings.get("ATL", "presets");
- const count = presets?.length;
- formData.name = `New Preset (${count + 1})`;
- }
+ /**
+ * Mimic changes to the Token document as if they were true document updates.
+ * @param {object} [changes] The changes to preview.
+ * @returns {void}
+ * @protected
+ */
+ _previewChanges(changes) {
+ if (!changes || !this._preview) return;
+ const deletions = { "-=actorId": null, "-=actorLink": null };
+ const mergeOptions = { inplace: false, performDeletions: true };
+ this._preview.updateSource(mergeObject(changes, deletions, mergeOptions));
+ }
- // Remove name change if updating a preset and trying to clear the name
- if (!this.newMode && "name" in formData && !formData.name) delete formData.name;
+ /* -------------------------------------------- */
+
+ /** @inheritDoc */
+ async _preparePartContext(partId, context, options) {
+ context = await super._preparePartContext(partId, context, options);
- return formData;
+ const tab = context.tabs[partId];
+ if (tab) {
+ context.tab = tab;
+ }
+ return context;
}
- async _onChangeInput(event) {
- super._onChangeInput(event);
+ /* -------------------------------------------- */
+
+
+ /** @inheritDoc */
+ _onChangeForm(formConfig, event) {
+ super._onChangeForm(formConfig, event);
// save the field's name that was changed
const el = event.target;
if (el.name) this.fieldsChanged.push(el.name);
// colorPicker has matching name in the dataset
else if (el.dataset.edit) this.fieldsChanged.push(el.dataset.edit);
+ console.log(this.fieldsChanged)
}
- async _updateObject(event, formData) {
- console.log("ATL |", "_updateObject called with formData:", formData);
- // apply the changes to the original preset
- Object.entries(formData)
- .filter(([k, _]) => this.fieldsChanged.includes(k))
- .forEach(([k, v]) => {
- if (v === "" || v === null) this._clearProperty(this.preset, k);
- else foundry.utils.setProperty(this.preset, k, v);
- });
- console.log("updated preset:", this.preset);
+ /* -------------------------------------------- */
- PresetConfig.savePreset(this.preset);
+ /**
+ * Add a new detection mode to the Token preview.
+ * @this {PresetConfig}
+ * @type {ApplicationClickAction}
+ */
+ static async #onAddDetectionMode() {
+ const formData = new FormDataExtended(this.form);
+ const modes = Object.values(this._processFormData(event, this.form, formData).detectionModes ?? {});
+ modes.push({ id: "", range: 0, enabled: true });
+ this._previewChanges({ detectionModes: modes });
+ await this.render({ parts: ["vision"], resetPreview: false });
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Remove a detection mode from the Token preview.
+ * @this {PresetConfig}
+ * @type {ApplicationClickAction}
+ */
+ static async #onRemoveDetectionMode(_event, target) {
+ const formData = new FormDataExtended(this.form);
+ const modes = Object.values(this._processFormData(event, this.form, formData).detectionModes ?? {});
+ const index = Number(target.closest("[data-index]")?.dataset.index);
+ modes.splice(index, 1);
+ this._previewChanges({ detectionModes: modes });
+ await this.render({ parts: ["vision"], resetPreview: false });
+
+ }
+
+ /** @inheritDoc */
+ _processFormData(event, form, formData) {
+ //const submitData = super._processFormData(event, form, formData);
+ const submitData = foundry.utils.expandObject(formData.object);
+ submitData.detectionModes ??= []; // Clear detection modes array
+ this._processChanges(submitData);
+ return submitData;
+ }
+
+ /**
+ * Process several fields from form submission data into proper model changes.
+ * @param {object} submitData Form submission data passed through {@link foundry.applications.ux.FormDataExtended}
+ * @protected
+ */
+ _processChanges(submitData) {
+ // Convert scale and mirror data from the form submission to TextureData changes
+ // if (typeof submitData.scale === "number") {
+ // submitData.texture.scaleX = submitData.scale * (submitData.mirrorX ? -1 : 1);
+ // submitData.texture.scaleY = submitData.scale * (submitData.mirrorY ? -1 : 1);
+ // }
+ // for (const key of ["scale", "mirrorX", "mirrorY"]) delete submitData[key];
+
+ // // Process token ring effects from the form submission
+ // if (Array.isArray(submitData.ring?.effects)) {
+ // const TRE = CONFIG.Token.ring.ringClass.effects;
+ // let effects = submitData.ring.enabled ? TRE.ENABLED : TRE.DISABLED;
+ // for (const effectName of submitData.ring.effects) {
+ // const v = TRE[effectName] ?? 0;
+ // effects |= v;
+ // }
+ // submitData.ring.effects = effects;
+ // }
}
+
_clearProperty(object, key) {
let target = object;
let cleared = false;
@@ -155,7 +400,7 @@ export class PresetConfig extends FormApplication {
// recursivly call to remove empty objects
if (parts) {
const remainingKey = parts.join(".");
- if (object[remainingKey] && isEmpty(object[remainingKey]))
+ if (object[remainingKey] && foundry.utils.isEmpty(object[remainingKey]))
this._clearProperty(object, remainingKey);
}
}
@@ -163,4 +408,5 @@ export class PresetConfig extends FormApplication {
// Return changed status
return cleared;
}
-}
+
+}
diff --git a/src/updateManager.js b/src/updateManager.js
index 47f6bc1..51477af 100644
--- a/src/updateManager.js
+++ b/src/updateManager.js
@@ -64,7 +64,7 @@ export class ATLUpdate {
if (change.key.includes("ATL")) {
changeFound = true;
updates.push(this.v9UpdateEffect(duplicate(change)))
- }
+ }
else updates.push(change);
}
if (changeFound) {
@@ -209,16 +209,16 @@ export class ATLUpdate {
return newData
}
- static async flagBuster(actor){
+ static async flagBuster(actor) {
console.warn(`Updating ${actor.name}`)
let flag = actor.getFlag("ATL", "originals")
- if(!flag) return ui.notifications.notify(`No Flag for ${actor.name}`)
- let updates = mergeObject(actor.data.token, flag, {inplace: false})
- await actor.update({token : updates})
+ if (!flag) return ui.notifications.notify(`No Flag for ${actor.name}`)
+ let updates = mergeObject(actor.data.token, flag, { inplace: false })
+ await actor.update({ token: updates })
}
- static async massFlagUpdate(){
- for(let actor of game.actors){
+ static async massFlagUpdate() {
+ for (let actor of game.actors) {
await this.flagBuster(actor)
}
}
@@ -351,7 +351,7 @@ export class ATLUpdate {
if (brightness !== 0)
changes.push({ key: "ATL.sight.brightness", value: brightness, mode, priority });
}
-
+
if (changeFound) return changes;
}
}
diff --git a/templates/appearance.hbs b/templates/appearance.hbs
new file mode 100644
index 0000000..97358e4
--- /dev/null
+++ b/templates/appearance.hbs
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{!--
+
+
+
+
+
{{localize "TOKEN.FIELDS.shape.hint"}}
+
--}}
+
+
+
+
+
+
{{localize "TOKEN.FIELDS.texture.fit.hint"}}
+
+
+
+
+
+
+
+
+
+
+
{{localize "TOKEN.AnchorHint"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{localize "TOKEN.FIELDS.lockRotation.hint"}}
+
+
+
+
\ No newline at end of file
diff --git a/templates/header.hbs b/templates/header.hbs
new file mode 100644
index 0000000..f34b6c6
--- /dev/null
+++ b/templates/header.hbs
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/identity.hbs b/templates/identity.hbs
new file mode 100644
index 0000000..20440f5
--- /dev/null
+++ b/templates/identity.hbs
@@ -0,0 +1,63 @@
+
\ No newline at end of file
diff --git a/templates/light.hbs b/templates/light.hbs
new file mode 100644
index 0000000..3ca865f
--- /dev/null
+++ b/templates/light.hbs
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/resources.hbs b/templates/resources.hbs
new file mode 100644
index 0000000..f44fe2a
--- /dev/null
+++ b/templates/resources.hbs
@@ -0,0 +1,10 @@
+
+ {{!-- --}}
+
diff --git a/templates/vision.hbs b/templates/vision.hbs
new file mode 100644
index 0000000..8383384
--- /dev/null
+++ b/templates/vision.hbs
@@ -0,0 +1,97 @@
+