diff --git a/.gitignore b/.gitignore index 4897802..28f81db 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ sync_to_module.sh test_sync_to_module.sh sync_module.sh + +*.sh diff --git a/Changelog.md b/Changelog.md index de12995..64e401c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,18 @@ +# 0.8.9 +### New features +Track combat moves on a per-combat basis. Add settings toggle to have movement speed highlighting reflect sum of moves for that combat round. + +### Bug fixes +Fix errors thrown when using the ruler without a movement token with either Terrain Mapper or Elevated Vision modules active. Closes issue #51. +Display speed colors to other users who have the speed color setting enabled. Closes issue #53. +Add CONFIG options to set additional movement types to the token hud. Closes issue #50. +```js +// Example: Add a swim movement to the api. +CONFIG.elevationruler.MOVEMENT_TYPES.SWIM = 3; // Increment by 1 from the highest-valued movement type +CONFIG.elevationruler.MOVEMENT_BUTTONS[CONFIG.elevationruler.MOVEMENT_TYPES.SWIM] = "person-swimming"; // From Font Awesome +CONFIG.elevationruler.SPEED.ATTRIBUTES.SWIM = "actor.system.attributes.movement.swim"; // dnd5e +``` + # 0.8.8 Improvements to updating the scene graph. Avoid leaving unneeded vertices and split edges when a token or wall is removed. Fixes to handling overlapping edges to correctly reflect what objects make up the edge. diff --git a/languages/en.json b/languages/en.json index b7ada02..05fe1f7 100644 --- a/languages/en.json +++ b/languages/en.json @@ -27,6 +27,9 @@ "elevationruler.settings.token-ruler-highlighting.name": "Use Token Speed Highlighting", "elevationruler.settings.token-ruler-highlighting.hint": "When using the ruler, use different colors for token walk/dash/max distance.", + "elevationruler.settings.token-ruler-combat-history.name": "Track Combat Move", + "elevationruler.settings.token-ruler-combat-history.hint": "For token speed highlighting, sum all the token moves within the round when displaying the highlight.", + "elevationruler.settings.token-speed-property.name": "Token Walk Property", "elevationruler.settings.token-speed-property.hint": "For token speed highlighting, this is the actor property representing token walking speed.", diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 485f2a6..5dc6a11 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -342,15 +342,28 @@ function _computeTokenSpeed(gridSpaces) { const walkDistance = tokenSpeed; const dashDistance = tokenSpeed * SPEED.MULTIPLIER; - // For each segment, determine the type of movement: walk, dash, max. - // If a segment has 2+ types, split the segment; recalculating distances. + // Variables changed in the loop let totalDistance = 0; let totalMoveDistance = 0; + let totalCombatMoveDistance = 0; + let prevCombatMoveDistance = 0; let dashing = false; let atMaximum = false; let nSegments = this.segments.length; - const maxIter = nSegments * 3; // Debugging + + // Add in already moved combat distance. + if ( game.combat?.started && Settings.get(Settings.KEYS.TOKEN_RULER.COMBAT_HISTORY) ) { + prevCombatMoveDistance = totalCombatMoveDistance = token.lastMoveDistance; + dashing = totalCombatMoveDistance >= walkDistance || totalCombatMoveDistance.almostEqual(walkDistance, .01); + atMaximum = totalCombatMoveDistance >= dashDistance || totalCombatMoveDistance.almostEqual(dashDistance, .01); + } + + // Debugging, to avoid infinite loops. + const maxIter = nSegments * 3; let iter = 0; + + // For each segment, determine the type of movement: walk, dash, max. + // If a segment has 2+ types, split the segment; recalculating distances. for ( let i = 0; i < nSegments; i += 1 ) { let segment = this.segments[i]; @@ -365,9 +378,9 @@ function _computeTokenSpeed(gridSpaces) { // Check if segment must be split. // Do dash first so the split can later be checked for maximum. - const newMoveDistance = totalMoveDistance + segment.moveDistance; - const targetDistance = (!dashing && newMoveDistance > walkDistance) ? walkDistance - : (!atMaximum && newMoveDistance > dashDistance) ? dashDistance + const newMoveDistance = totalCombatMoveDistance + segment.moveDistance; + const targetDistance = (!dashing && newMoveDistance > walkDistance) ? (walkDistance - prevCombatMoveDistance) + : (!atMaximum && newMoveDistance > dashDistance) ? (dashDistance - prevCombatMoveDistance) : undefined; if ( targetDistance ) { // Force dash and maximum, to avoid loops on error in measurement. @@ -387,13 +400,14 @@ function _computeTokenSpeed(gridSpaces) { totalDistance += segment.distance; totalMoveDistance += segment.moveDistance; + totalCombatMoveDistance += segment.moveDistance; // Mark segment speed and flag when past the dash and maximum points. - if ( totalMoveDistance > dashDistance && !totalMoveDistance.almostEqual(dashDistance, .01) ) { + if ( totalCombatMoveDistance > dashDistance && !totalCombatMoveDistance.almostEqual(dashDistance, .01) ) { segment.speed = SPEED.TYPES.MAXIMUM; dashing ||= true; atMaximum ||= true; - } else if ( totalMoveDistance > walkDistance && !totalMoveDistance.almostEqual(walkDistance, .01) ) { + } else if ( totalCombatMoveDistance > walkDistance && !totalCombatMoveDistance.almostEqual(walkDistance, .01) ) { segment.speed = SPEED.TYPES.DASH; dashing ||= true; } else segment.speed = SPEED.TYPES.WALK; diff --git a/scripts/Token.js b/scripts/Token.js index 1282c28..05e113f 100644 --- a/scripts/Token.js +++ b/scripts/Token.js @@ -77,12 +77,16 @@ 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. - * @param {boolean} [sinceCombatTurn=true] Should the combat turn zero out the movement distance. * @returns {number} */ function lastMoveDistance() { - if ( game.combat?.active && this._lastCombatRoundMove < game.combat.round ) return 0; - return this._lastMoveDistance ?? 0; + 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; + return combatData.lastMoveDistance; + } + return this._lastMoveDistance || 0; } /** @@ -97,10 +101,20 @@ function updateToken(document, changes, _options, _userId) { if ( token.isPreview || !(Object.hasOwn(changes, "x")|| Object.hasOwn(changes, "y") || Object.hasOwn(changes, "elevation")) ) return; - if ( game.combat?.active ) token._lastCombatRoundMove = game.combat.round; const ruler = canvas.controls.ruler; if ( ruler.active && ruler._getMovementToken() === token ) token._lastMoveDistance = ruler.totalMoveDistance; else token._lastMoveDistance = Ruler.measureMoveDistance(token.position, token.document, 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; + } } /** diff --git a/scripts/const.js b/scripts/const.js index ae99663..9bfcb76 100644 --- a/scripts/const.js +++ b/scripts/const.js @@ -46,9 +46,6 @@ export const MOVEMENT_TYPES = { FLY: 2 }; -// Store the flipped key/values. -Object.entries(MOVEMENT_TYPES).forEach(([key, value]) => MOVEMENT_TYPES[value] = key); - export const MOVEMENT_BUTTONS = { [MOVEMENT_TYPES.AUTO]: "road-lock", [MOVEMENT_TYPES.BURROW]: "person-digging", @@ -56,12 +53,6 @@ export const MOVEMENT_BUTTONS = { [MOVEMENT_TYPES.FLY]: "dove" }; -export const SPEED_ATTRIBUTES = { - [MOVEMENT_TYPES.BURROW]: "", - [MOVEMENT_TYPES.WALK]: "", - [MOVEMENT_TYPES.FLY]: "" -}; - /** * Below Taken from Drag Ruler */ diff --git a/scripts/measure_distance.js b/scripts/measure_distance.js index b4e6551..d1093f9 100644 --- a/scripts/measure_distance.js +++ b/scripts/measure_distance.js @@ -709,7 +709,7 @@ function hasActiveDrawingTerrain(drawing, currElev, prevElev) { * @returns {number} Percent move penalty to apply. Returns 1 if no penalty. */ function griddedTerrainMovePenalty(token, currGridCoords, prevGridCoords, currElev = 0, prevElev = 0) { - if ( !MODULES_ACTIVE.TERRAIN_MAPPER ) return 1; + if ( !MODULES_ACTIVE.TERRAIN_MAPPER || !token ) return 1; const Terrain = MODULES_ACTIVE.API.TERRAIN_MAPPER.Terrain; const speedAttribute = SPEED.ATTRIBUTES[token.movementType] ?? SPEED.ATTRIBUTES.WALK; const GT = Settings.KEYS.GRID_TERRAIN; diff --git a/scripts/module.js b/scripts/module.js index 36c002c..4548156 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -41,9 +41,18 @@ Hooks.once("init", function() { // Font awesome identifiers for the Token HUD speed selection. MOVEMENT_BUTTONS, + // Types of movement. + MOVEMENT_TYPES, + debug: false }; + /* To add a movement to the api: + CONFIG.elevationruler.MOVEMENT_TYPES.SWIM = 3; // Increment by 1 from the highest-valued movement type + CONFIG.elevationruler.MOVEMENT_BUTTONS[CONFIG.elevationruler.MOVEMENT_TYPES.SWIM] = "person-swimming"; // From Font Awesome + CONFIG.elevationruler.SPEED.ATTRIBUTES.SWIM = "actor.system.attributes.movement.swim"; // dnd5e + */ + game.modules.get(MODULE_ID).api = { iterateGridUnderLine, @@ -52,7 +61,6 @@ Hooks.once("init", function() { sumGridMoves, gridShapeFromGridCoords, PATCHER, - MOVEMENT_TYPES, // Pathfinding pathfinding: { diff --git a/scripts/segments.js b/scripts/segments.js index 8c4c42c..d577b3a 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -289,7 +289,7 @@ export function _highlightMeasurementSegment(wrapped, segment) { const priorColor = this.color; const token = this._getMovementToken(); const doSpeedHighlighting = token - && this.user === game.user + // && this.user === game.user && Settings.get(Settings.KEYS.TOKEN_RULER.SPEED_HIGHLIGHTING); // Highlight each split in turn, changing highlight color each time. diff --git a/scripts/settings.js b/scripts/settings.js index 0272c53..9c58ac2 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -42,7 +42,8 @@ const SETTINGS = { ENABLED: "enable-token-ruler", ROUND_TO_MULTIPLE: "round-to-multiple", SPEED_HIGHLIGHTING: "token-ruler-highlighting", - TOKEN_MULTIPLIER: "token-terrain-multiplier" + TOKEN_MULTIPLIER: "token-terrain-multiplier", + COMBAT_HISTORY: "token-ruler-combat-history" }, GRID_TERRAIN: { @@ -54,7 +55,6 @@ const SETTINGS = { }, AREA_THRESHOLD: "grid-terrain-area-threshold" } - }; const KEYBINDINGS = { @@ -170,6 +170,16 @@ export class Settings extends ModuleSettingsAbstract { requiresReload: false }); + register(KEYS.TOKEN_RULER.COMBAT_HISTORY, { + name: localize(`${KEYS.TOKEN_RULER.COMBAT_HISTORY}.name`), + hint: localize(`${KEYS.TOKEN_RULER.COMBAT_HISTORY}.hint`), + scope: "user", + config: true, + default: false, + type: Boolean, + requiresReload: false + }); + register(KEYS.TOKEN_RULER.ROUND_TO_MULTIPLE, { name: localize(`${KEYS.TOKEN_RULER.ROUND_TO_MULTIPLE}.name`), hint: localize(`${KEYS.TOKEN_RULER.ROUND_TO_MULTIPLE}.hint`), diff --git a/scripts/terrain_elevation.js b/scripts/terrain_elevation.js index 1c3a1e1..acd84cc 100644 --- a/scripts/terrain_elevation.js +++ b/scripts/terrain_elevation.js @@ -43,6 +43,7 @@ Used by ruler to get elevation at waypoints and at the end of the ruler. import { MODULES_ACTIVE } from "./const.js"; import { Settings } from "./settings.js"; +import { Point3d } from "./geometry/3d/Point3d.js"; /** * Calculate the elevation for a given waypoint. diff --git a/scripts/token_hud.js b/scripts/token_hud.js index 9b056a8..291c399 100644 --- a/scripts/token_hud.js +++ b/scripts/token_hud.js @@ -31,6 +31,7 @@ SOFTWARE. import { MODULE_ID, FLAGS, MOVEMENT_TYPES, MOVEMENT_BUTTONS, MODULES_ACTIVE } from "./const.js"; import { LevelsElevationAtPoint } from "./terrain_elevation.js"; +import { keyForValue } from "./util.js"; export const PATCHES = {}; PATCHES.MOVEMENT_SELECTION = {}; @@ -56,7 +57,7 @@ PATCHES.MOVEMENT_SELECTION.HOOKS = { renderTokenHUD }; function movementType() { let selectedMovement = this.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_SELECTION); if ( selectedMovement === MOVEMENT_TYPES.AUTO ) return determineMovementType(this); - return MOVEMENT_TYPES[selectedMovement]; + return keyForValue(MOVEMENT_TYPES, selectedMovement); } PATCHES.MOVEMENT_SELECTION.GETTERS = { movementType }; @@ -74,7 +75,7 @@ function determineMovementType(token) { groundElevation = LevelsElevationAtPoint(token.center, { startingElevation: token.elevationE }) ?? 0; } else groundElevation = 0; - return MOVEMENT_TYPES[Math.sign(token.elevationE - groundElevation) + 1]; + return keyForValue(MOVEMENT_TYPES, Math.sign(token.elevationE - groundElevation) + 1); } /** @@ -97,7 +98,7 @@ function addMovementSelectionButton(tokenDocument, html) { */ async function onMovementTypeButtonClick(tokenDocument, html) { const currentType = tokenDocument.getFlag(MODULE_ID, FLAGS.MOVEMENT_SELECTION); // May be undefined. - const nextTypeName = MOVEMENT_TYPES[currentType + 1] ?? "AUTO"; + const nextTypeName = keyForValue(MOVEMENT_TYPES, currentType + 1) ?? "AUTO"; await tokenDocument.setFlag(MODULE_ID, FLAGS.MOVEMENT_SELECTION, MOVEMENT_TYPES[nextTypeName]); html.find("#switch-movement-type").remove(); addMovementSelectionButton(tokenDocument, html); diff --git a/scripts/util.js b/scripts/util.js index b4567f5..92738df 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -243,4 +243,11 @@ export function filterSplice(arr, filterFn) { indices.sort((a, b) => b - a); // So we can splice without changing other indices. indices.forEach(idx => arr.splice(idx, 1)); return filteredElems; +} + +/** + * Get the key for a given object value. Presumes unique values, otherwise returns first. + */ +export function keyForValue(object, value) { + return Object.keys(object).find(key => object[key] === value); } \ No newline at end of file