From 002ef4ccf1d7cd54d7f6149c660494f827ac5d01 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Thu, 11 Jul 2024 12:16:40 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=B8=20feat=20|Token|Use=20Foundry=20un?= =?UTF-8?q?do=20system=20to=20track=20move=20history?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Store token move history to flag on the token. Now when the token move is undone, the flag is automatically changed back. --- scripts/Token.js | 76 +++++++++++++++++++++++++++++-------------- scripts/TokenLayer.js | 64 ------------------------------------ scripts/const.js | 3 +- scripts/patching.js | 2 -- 4 files changed, 54 insertions(+), 91 deletions(-) delete mode 100644 scripts/TokenLayer.js diff --git a/scripts/Token.js b/scripts/Token.js index 59a1bf7..7cd4e7e 100644 --- a/scripts/Token.js +++ b/scripts/Token.js @@ -1,11 +1,13 @@ /* globals canvas, CanvasAnimation, +foundry, game, Ruler */ /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ +import { MODULE_ID, FLAGS } from "./const.js"; import { Settings } from "./settings.js"; import { log } from "./util.js"; @@ -18,33 +20,44 @@ PATCHES.PATHFINDING = {}; // ----- NOTE: Hooks ----- // /** - * Hook updateToken to track token movement. - * @param {Document} document The existing Document which was updated - * @param {object} change Differential data that was used to update the document - * @param {DocumentModificationContext} options Additional options which modified the update request - * @param {string} userId The ID of the User who triggered the update workflow + * Hook preUpdateToken to track token movement + * @param {Document} document The Document instance being updated + * @param {object} changed Differential data that will be used to update the document + * @param {Partial} options Additional options which modify the update request + * @param {string} userId The ID of the requesting user, always game.user.id + * @returns {boolean|void} Explicitly return false to prevent update of this Document */ -function updateToken(document, changes, _options, _userId) { +function preUpdateToken(document, changes, _options, _userId) { const token = document.object; if ( token.isPreview - || !(Object.hasOwn(changes, "x")|| Object.hasOwn(changes, "y") || Object.hasOwn(changes, "elevation")) ) return; + || !(Object.hasOwn(changes, "x") || Object.hasOwn(changes, "y") || Object.hasOwn(changes, "elevation")) ) return; + // Don't update move data if the move flag is being updated (likely due to control-z undo). + if ( foundry.utils.hasProperty(changes, `flags.${MODULE_ID}.${FLAGS.MOVEMENT_HISTORY}`) ) return; + + // Store the move data in a token flag so it survives reloads and can be updated on control-z undo by another user. + // First determine the current move data. + let lastMoveDistance = 0; + let combatMoveData = {}; const ruler = canvas.controls.ruler; - if ( ruler.active && ruler.token === token ) token._lastMoveDistance = ruler.totalMoveDistance; - else token._lastMoveDistance = Ruler.measureMoveDistance(token.position, token.document._source, { token }).moveDistance; + if ( ruler.active && ruler.token === token ) lastMoveDistance = ruler.totalMoveDistance; + else lastMoveDistance = Ruler.measureMoveDistance(token.position, token.document._source, { token }).moveDistance; if ( game.combat?.started ) { // Store the combat move distance and the last round for which the combat move occurred. // Map to each unique combat. - const combatId = game.combat.id; - token._combatMoveData ??= new Map(); - if ( !token._combatMoveData.has(combatId) ) { - token._combatMoveData.set(combatId, { lastMoveDistance: 0, lastRound: -1 }); - } - const combatData = token._combatMoveData.get(combatId); - if ( combatData.lastRound < game.combat.round ) combatData.lastMoveDistance = token._lastMoveDistance; - else combatData.lastMoveDistance += token._lastMoveDistance; + const combatData = {...token._combatMoveData}; + if ( combatData.lastRound < game.combat.round ) combatData.lastMoveDistance = lastMoveDistance; + else combatData.lastMoveDistance += lastMoveDistance; combatData.lastRound = game.combat.round; + combatMoveData = { [game.combat.id]: combatData }; } + + // Combine with existing move data in the token flag. + const flagData = document.getFlag(MODULE_ID, FLAGS.MOVEMENT_HISTORY) ?? {}; + foundry.utils.mergeObject(flagData, { lastMoveDistance, combatMoveData }); + + // Update the flag with the new data. + foundry.utils.setProperty(changes, `flags.${MODULE_ID}.${FLAGS.MOVEMENT_HISTORY}`, flagData); } // ----- NOTE: Wraps ----- // @@ -139,16 +152,31 @@ async function _onDragLeftDrop(wrapped, event) { * Token.prototype.lastMoveDistance * Return the last move distance. If combat is active, return the last move since this token * started its turn. - * @returns {number} + * @type {number} */ function lastMoveDistance() { if ( game.combat?.started ) { - if ( !this._combatMoveData ) return 0; - const combatData = this._combatMoveData.get(game.combat.id); - if ( !combatData || combatData.lastRound < game.combat.round ) return 0; + const combatData = this._combatMoveData; + if ( combatData.lastRound < game.combat.round ) return 0; return combatData.lastMoveDistance; } - return this._lastMoveDistance || 0; + return this.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_HISTORY)?.lastMoveDistance || 0; +} + +/** + * Token.prototype._combatData + * Map that stores the combat move data. + * Constructed from the relevant flag. + * @type {object} + * - @prop {number} lastMoveDistance Distance of last move during combat round + * - @prop {number} lastRound The combat round in which the last move occurred + */ +function _combatMoveData() { + const combatId = game.combat?.id; + const defaultData = { lastMoveDistance: 0, lastRound: -1 }; + if ( typeof combatId === "undefined" ) return defaultData; + const combatMoveData = this.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_HISTORY)?.combatMoveData ?? { }; + return combatMoveData[combatId] ?? defaultData; } // ----- NOTE: Patches ----- // @@ -162,8 +190,8 @@ PATCHES.PATHFINDING.WRAPS = { _onUpdate }; PATCHES.TOKEN_RULER.MIXES = { _onDragLeftDrop, _onDragLeftCancel }; -PATCHES.MOVEMENT_TRACKING.HOOKS = { updateToken }; -PATCHES.MOVEMENT_TRACKING.GETTERS = { lastMoveDistance }; +PATCHES.MOVEMENT_TRACKING.HOOKS = { preUpdateToken }; +PATCHES.MOVEMENT_TRACKING.GETTERS = { lastMoveDistance, _combatMoveData }; // ----- NOTE: Helper functions ----- // diff --git a/scripts/TokenLayer.js b/scripts/TokenLayer.js deleted file mode 100644 index 76b4609..0000000 --- a/scripts/TokenLayer.js +++ /dev/null @@ -1,64 +0,0 @@ -/* globals -canvas, -foundry -*/ -/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ - -import { MODULE_ID } from "./const.js"; - -// Patches for the TokenLayer class -export const PATCHES = {}; -PATCHES.MOVEMENT_TRACKING = {}; - -// ----- NOTE: Wraps ----- // - -/** - * Wrap TokenLayer.prototype.storeHistory - * Add token movement data if the token moved so combat move history can be undone. - * @param {string} type The event type (create, update, delete) - * @param {Object[]} data The object data - */ -function storeHistory(wrapped, type, data) { - wrapped(type, data); - if ( type === "create" ) return; - data = data.filter(d => Object.keys(d).length > 1); // Filter entries without changes - if ( !data.length ) return; - const addedObj = this.history.at(-1); - const tokenMap = new Map(canvas.tokens.placeables.map(token => [token.id, token])); - for ( const datum of data ) { - addedObj[MODULE_ID] ??= {}; - const token = tokenMap.get(datum._id); - if ( !token ) continue; - - // Copy the current move data. - token._combatMoveData ??= new Map(); - const lastMoveDistance = token._lastMoveDistance ?? 0; - const combatMoveData = foundry.utils.duplicate([...token._combatMoveData.entries()]); - addedObj[MODULE_ID][datum._id] = { lastMoveDistance, combatMoveData }; - } -} - -/** - * Wrap TokenLayer.prototype.undoHistory - * Reset the tokens' movement data to the previous history. - * @returns {Promise} An array of documents which were modified by the undo operation - */ -async function undoHistory(wrapped) { - const event = this.history.at(-1); - const res = await wrapped(); - if ( !event || event.type === "create" ) return res; // Create would be undone with deletion, which we can ignore. - - // If deletion event, a new token would be created upon undo. Update with its previous event movement data. - // If update event, update with previous event movement data. - if ( !event[MODULE_ID] ) return res; - const tokenMap = new Map(canvas.tokens.placeables.map(token => [token.id, token])); - for ( const [id, { lastMoveDistance, combatMoveData }] of Object.entries(event[MODULE_ID]) ) { - const token = tokenMap.get(id); - if ( !token ) continue; - token._lastMoveDistance = lastMoveDistance; - token._combatMoveData = new Map(combatMoveData); - } - return res; -} - -PATCHES.MOVEMENT_TRACKING.WRAPS = { storeHistory, undoHistory }; diff --git a/scripts/const.js b/scripts/const.js index 29f2db1..c3434c7 100644 --- a/scripts/const.js +++ b/scripts/const.js @@ -17,7 +17,8 @@ export const FLAGS = { MOVEMENT_PENALTY: "movementPenalty", SCENE: { BACKGROUND_ELEVATION: "backgroundElevation" - } + }, + MOVEMENT_HISTORY: "movementHistory" }; export const MODULES_ACTIVE = { API: {} }; diff --git a/scripts/patching.js b/scripts/patching.js index 288c64e..0802073 100644 --- a/scripts/patching.js +++ b/scripts/patching.js @@ -18,7 +18,6 @@ import { PATCHES as PATCHES_TokenPF } from "./pathfinding/Token.js"; // Movement tracking import { PATCHES as PATCHES_TokenHUD } from "./token_hud.js"; -import { PATCHES as PATCHES_TokenLayer } from "./TokenLayer.js"; // Settings import { PATCHES as PATCHES_ClientSettings } from "./ModuleSettingsAbstract.js"; @@ -32,7 +31,6 @@ const PATCHES = { DrawingConfig: PATCHES_DrawingConfig, Ruler: PATCHES_Ruler, Token: mergeObject(mergeObject(PATCHES_Token, PATCHES_TokenPF), PATCHES_TokenHUD), - TokenLayer: PATCHES_TokenLayer, Wall: PATCHES_Wall };