Skip to content

Commit

Permalink
Merge branch 'release/0.9.9' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
caewok committed Jul 12, 2024
2 parents 43d2f41 + 53a1551 commit c8a62b0
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 60 deletions.
7 changes: 7 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 0.9.9
Moved token movement history to a flag, which now takes advantage of the Foundry undo system so that when undoing token movement during combat, the token's movement history is reset accordingly.
Added a button in the Combat Tracker that GMs can use to reset the current combatant movement history during combat. Closes #89.

Fix for ruler display ghosting on other users' displays. Closes #124, #134.
Fix for elevation changing too much when using hotkeys. Closes #126.

# 0.9.8
Fix for `FLAGS` not defined error.
Round prior movement label.
Expand Down
4 changes: 3 additions & 1 deletion languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,7 @@
"elevationruler.controls.pathfinding-control.name": "Use Pathfinding",

"elevationruler.drawingconfig.movementPenalty.name": "Movement Bonus/Penalty",
"elevationruler.drawingconfig.movementPenalty.hint": "Set to 1 for no penalty. Values greater than one penalize movement by that percent; values less than one effectively grant a bonus to movement. For example, set to 2 to double movement through this area."
"elevationruler.drawingconfig.movementPenalty.hint": "Set to 1 for no penalty. Values greater than one penalize movement by that percent; values less than one effectively grant a bonus to movement. For example, set to 2 to double movement through this area.",

"elevationruler.clearMovement": "Clear Combatant Movement"
}
56 changes: 56 additions & 0 deletions scripts/CombatTracker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* globals
game
*/
/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */

import { TEMPLATES, MODULE_ID, FLAGS } from "./const.js";

// Patches for the Combat tracker

export const PATCHES = {};
PATCHES.BASIC = {};

import { injectConfiguration, renderTemplateSync } from "./util.js";

// ----- NOTE: Hooks ----- //

/**
* Hook renderCombatTracker
* Add a button at the top left to clear the current token's movement.
* @param {Application} application The Application instance being rendered
* @param {jQuery} html The inner HTML of the document that will be displayed and may be modified
* @param {object} data The object of data used when rendering the application
*/
function renderCombatTracker(app, html, data) {
if ( !game.user.isGM ) return;
const encounterControlsDiv = html.find(".encounter-controls")[0];
if ( !encounterControlsDiv ) return;
const combatButtons = encounterControlsDiv.getElementsByClassName("combat-button");
if ( !combatButtons.length ) return;
const dividers = encounterControlsDiv.getElementsByTagName("h3");
if ( !dividers.length ) return;

const myHtml = renderTemplateSync(TEMPLATES.COMBAT_TRACKER, data);
// const aElem = document.createElement("a");
// aElem.innerHTML = myHtml;
dividers[0].insertAdjacentHTML("beforebegin", myHtml);

// const npcButton = Object.values(combatButtons).findIndex(b => b.dataset.control === "rollNPC");
// const findString = ".combat-button[data-control='rollNPC']";
// await injectConfiguration(app, html, data, template, findString);

html.find(`.${MODULE_ID}`).click(ev => clearMovement.call(app, ev));
}

PATCHES.BASIC.HOOKS = { renderCombatTracker };

async function clearMovement(event) {
event.preventDefault();
event.stopPropagation();
const combat = this.viewed;
const tokenD = combat?.combatant?.token;
if ( !tokenD ) return;
await tokenD.unsetFlag(MODULE_ID, FLAGS.MOVEMENT_HISTORY);
ui.notifications.notify(`Combat movement history for ${tokenD.name} reset.`);

}
2 changes: 1 addition & 1 deletion scripts/Ruler.js
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,8 @@ export function measureSegment(segment, token, numPrevDiagonal = 0) {
*/
function _broadcastMeasurement(wrapped) {
// Don't broadcast invisible, hidden, or secret token movement when dragging.
if ( this._isTokenRuler && !this.token ) return;
if ( this._isTokenRuler
&& this.token
&& (this.token.document.disposition === CONST.TOKEN_DISPOSITIONS.SECRET
|| this.token.document.hasStatusEffect(CONFIG.specialStatusEffects.INVISIBLE)
|| this.token.document.isHidden) ) return;
Expand Down
155 changes: 101 additions & 54 deletions scripts/Token.js
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -15,6 +17,51 @@ PATCHES.TOKEN_RULER = {}; // Assume this patch is only present if the token rule
PATCHES.MOVEMENT_TRACKING = {};
PATCHES.PATHFINDING = {};

// ----- NOTE: Hooks ----- //

/**
* 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<DatabaseUpdateOperation>} 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 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;

// 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 ) 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 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 ----- //

/**
* Wrap Token.prototype._onDragLeftStart
* Start a ruler measurement.
Expand Down Expand Up @@ -67,6 +114,18 @@ function _onDragLeftMove(wrapped, event) {
if ( ruler._state > 0 ) ruler._onMouseMove(event);
}

/**
* Wrap Token.prototype._onUpdate to remove easing for pathfinding segments.
*/
function _onUpdate(wrapped, data, options, userId) {
if ( options?.rulerSegment && options?.animation?.easing ) {
options.animation.easing = options.firstRulerSegment ? noEndEase(options.animation.easing)
: options.lastRulerSegment ? noStartEase(options.animation.easing)
: undefined;
}
return wrapped(data, options, userId);
}

/**
* Mix Token.prototype._onDragLeftDrop
* End the ruler measurement.
Expand All @@ -87,73 +146,40 @@ async function _onDragLeftDrop(wrapped, event) {
ruler._onMoveKeyDown(event); // Movement is async here but not awaited in _onMoveKeyDown.
}

// ----- NOTE: New getters ----- //

/**
* 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;
}

/**
* 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
*/
function updateToken(document, changes, _options, _userId) {
const token = document.object;
if ( token.isPreview
|| !(Object.hasOwn(changes, "x")|| Object.hasOwn(changes, "y") || Object.hasOwn(changes, "elevation")) ) return;

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 ( 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;
combatData.lastRound = game.combat.round;
}
return this.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_HISTORY)?.lastMoveDistance || 0;
}

/**
* Wrap Token.prototype._onUpdate to remove easing for pathfinding segments.
* 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 _onUpdate(wrapped, data, options, userId) {
if ( options?.rulerSegment && options?.animation?.easing ) {
options.animation.easing = options.firstRulerSegment ? noEndEase(options.animation.easing)
: options.lastRulerSegment ? noStartEase(options.animation.easing)
: undefined;
}
return wrapped(data, options, userId);
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;
}

function noStartEase(easing) {
if ( typeof easing === "string" ) easing = CanvasAnimation[easing];
return pt => (pt < 0.5) ? pt : easing(pt);
}

function noEndEase(easing) {
if ( typeof easing === "string" ) easing = CanvasAnimation[easing];
return pt => (pt > 0.5) ? pt : easing(pt);
}
// ----- NOTE: Patches ----- //

PATCHES.TOKEN_RULER.WRAPS = {
_onDragLeftStart,
Expand All @@ -164,6 +190,27 @@ 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 ----- //

/**
* For given easing function, modify it so it does not ease for the first half of the move.
* @param {function} easing
* @returns {function}
*/
function noStartEase(easing) {
if ( typeof easing === "string" ) easing = CanvasAnimation[easing];
return pt => (pt < 0.5) ? pt : easing(pt);
}

/**
* For given easing function, modify it so it does not ease for the second half of the move.
* @param {function} easing
* @returns {function}
*/
function noEndEase(easing) {
if ( typeof easing === "string" ) easing = CanvasAnimation[easing];
return pt => (pt > 0.5) ? pt : easing(pt);
}
6 changes: 4 additions & 2 deletions scripts/const.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ export const MODULE_ID = "elevationruler";
export const EPSILON = 1e-08;

export const TEMPLATES = {
DRAWING_CONFIG: `modules/${MODULE_ID}/templates/drawing-config.html`
DRAWING_CONFIG: `modules/${MODULE_ID}/templates/drawing-config.html`,
COMBAT_TRACKER: `modules/${MODULE_ID}/templates/combat-tracker.html`
};

export const FLAGS = {
MOVEMENT_SELECTION: "selectedMovementType",
MOVEMENT_PENALTY: "movementPenalty",
SCENE: {
BACKGROUND_ELEVATION: "backgroundElevation"
}
},
MOVEMENT_HISTORY: "movementHistory"
};

export const MODULES_ACTIVE = { API: {} };
Expand Down
4 changes: 3 additions & 1 deletion scripts/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ui

import { Settings } from "./settings.js";
import { initializePatching, PATCHER } from "./patching.js";
import { MODULE_ID, MOVEMENT_TYPES, MOVEMENT_BUTTONS, SPEED } from "./const.js";
import { MODULE_ID, MOVEMENT_TYPES, MOVEMENT_BUTTONS, SPEED, TEMPLATES } from "./const.js";
import { defaultHPAttribute } from "./system_attributes.js";
import { registerGeometry } from "./geometry/registration.js";

Expand Down Expand Up @@ -185,6 +185,8 @@ Hooks.once("init", function() {

Settings
};

loadTemplates(Object.values(TEMPLATES)).then(_value => log(`Templates loaded.`)); // eslint-disable-line no-unused-vars
});

// Setup is after init; before ready.
Expand Down
2 changes: 2 additions & 0 deletions scripts/patching.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { PATCHES as PATCHES_TokenPF } from "./pathfinding/Token.js";

// Movement tracking
import { PATCHES as PATCHES_TokenHUD } from "./token_hud.js";
import { PATCHES as PATCHES_CombatTracker } from "./CombatTracker.js";

// Settings
import { PATCHES as PATCHES_ClientSettings } from "./ModuleSettingsAbstract.js";
Expand All @@ -27,6 +28,7 @@ const mergeObject = foundry.utils.mergeObject;
const PATCHES = {
ClientKeybindings: PATCHES_ClientKeybindings,
ClientSettings: PATCHES_ClientSettings,
CombatTracker: PATCHES_CombatTracker,
["foundry.canvas.edges.CanvasEdges"]: PATCHES_CanvasEdges,
DrawingConfig: PATCHES_DrawingConfig,
Ruler: PATCHES_Ruler,
Expand Down
7 changes: 6 additions & 1 deletion scripts/segments.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ export function _getSegmentLabel(wrapped, segment, totalDistance) {
const levelName = levelNameAtElevation(CONFIG.GeometryLib.utils.pixelsToGridUnits(segment.ray.B.z));
if ( levelName ) elevLabel += `\n${levelName}`;

if ( CONFIG[MODULE_ID].debug ) {
if ( totalDistance > 30 ) { console.debug("_getSegmentLabel: 30", segment, this); }
else if ( totalDistance > 60 ) { console.debug("_getSegmentLabel: 30", segment, this); }
}

let moveLabel = "";
const units = (canvas.scene.grid.units) ? ` ${canvas.scene.grid.units}` : "";
if ( segment.waypointDistance !== segment.waypointMoveDistance ) {
Expand Down Expand Up @@ -401,7 +406,7 @@ function segmentElevationLabel(s) {
const units = canvas.scene.grid.units;
const increment = s.waypointElevationIncrement;
const multiple = Settings.get(Settings.KEYS.TOKEN_RULER.ROUND_TO_MULTIPLE) || 1;
const elevation = CONFIG.GeometryLib.utils.pixelsToGridUnits(s.ray.B.z).toNearest(multiple);
const elevation = (CONFIG.GeometryLib.utils.pixelsToGridUnits(s.ray.A.z) + s.waypointElevationIncrement).toNearest(multiple);

const segmentArrow = (increment > 0) ? "↑"
: (increment < 0) ? "↓" : "↕";
Expand Down
17 changes: 17 additions & 0 deletions scripts/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,20 @@ export function * iterateGridUnderLine(origin, destination, { reverse = false }
tPrior = t;
}
}

/**
* Synchronous version of renderTemplate.
* Requires the template to be already loaded.
* @param {string} path The file path to the target HTML template
* @param {Object} data A data object against which to compile the template
* @returns {string|undefined} Returns the compiled and rendered template as a string
*/
export function renderTemplateSync(path, data) {
if ( !Object.hasOwn(Handlebars.partials, path) ) return;
const template = Handlebars.partials[path];
return template(data || {}, {
allowProtoMethodsByDefault: true,
allowProtoPropertiesByDefault: true
});
}

3 changes: 3 additions & 0 deletions templates/combat-tracker.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<a class="combat-button elevationruler" aria-label="{{localize 'elevationruler.clearMovement'}}" role="button" data-tooltip="elevationruler.clearMovement" data-control="clearMovement" {{#unless turns}}disabled{{/unless}}>
<i class="fas fa-person-walking-arrow-loop-left"></i>
</a>

0 comments on commit c8a62b0

Please sign in to comment.