diff --git a/Changelog.md b/Changelog.md index a244f09..784adcb 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,14 @@ +# 0.10.6 +## New Features +Setting to apply movement penalties per-grid-space. Defaults to enabled. For gridded scenes, this will (1) count regions/tokens/drawings as imposing movement penalty only if the region/token/drawing overlaps the center point of the grid space and (2) impose the penalty at a grid-space level. If disabled, this will proportionally apply the penalty based on the precise movement path. Closes #181. + +Setting to add (or subtract) a flat amount when moving into a grid space with a token penalty. E.g. +5 per grid square. Add parallel setting to drawing configuration. (May add a similar toggle to regions in Terrain Mapper if this cannot be easily handled using active effects.) Closes #125. + +## Bug fixes and other updates +Correct NaN distance when using hex grids. Closes #188. +Don't treat walls as blocking if the top elevation equals the token elevation. Closes #189. +Update Italian localization. Thanks @GregoryWarn! + # 0.10.5 Fix for `_fromPoint3d` not a function. Closes #186. diff --git a/languages/en.json b/languages/en.json index 7daa5d8..ee922d1 100644 --- a/languages/en.json +++ b/languages/en.json @@ -76,8 +76,11 @@ "elevationruler.settings.round-to-multiple.name": "Round Distance to Multiple", "elevationruler.settings.round-to-multiple.hint": "Round the distance displayed to the nearest multiple of the number entered. Set to 0 to disable rounding.", - "elevationruler.settings.token-terrain-multiplier.name": "Token as Terrain Multiplier", - "elevationruler.settings.token-terrain-multiplier.hint": "Multiplier to use to calculate movement speed when moving through other tokens. Set to 1 to ignore. Values less than 1 treat token spaces as faster than normal; values greater than 1 penalize movement through token spaces.", + "elevationruler.settings.token-terrain-multiplier.name": "Token Penalty Percent", + "elevationruler.settings.token-terrain-multiplier.hint": "Penalize movement through other tokens. Set to 1 for no penalty, less than one to grant a bonus, greater than 1 to impose a penalty multiplier. For example, set to 2 to double movement cost through a token.", + + "elevationruler.settings.token-terrain-multiplier-flat.name": "Flat Token Penalty", + "elevationruler.settings.token-terrain-multiplier-flat.hint": "If enabled, treats the token move penalty as a fixed amount of additional distance. For example, set to 5 to add +5 to each square of movement. Negative values provide a flat bonus.", "elevationruler.settings.pathfinding_enable.name": "Use Pathfinding", "elevationruler.settings.pathfinding_enable.hint": "When enabled, adds a pathfinding togglee to the Token controls that will cause the ruler to map a path around walls and tokens, depending on settings. Disable this if pathfinding is causing compatibility issues. Disabling may also result in a small performance increase.", @@ -96,6 +99,7 @@ "elevationruler.settings.grid-terrain-algorithm.name": "Terrain Grid Measurement", "elevationruler.settings.grid-terrain-algorithm.hint": "When on a grid, how to account for movement penalties or bonuses from terrain and tokens? Center: apply if the terrain/token overlaps the grid center; Percent Area: apply if the terrain/token covers at least this much of the grid square/hex; Euclidean: prorate based on percent of line segment within the terrain/token between this grid square/hex and the previous.", + "elevationruler.settings.grid-terrain-choice-center-point": "Center Point", "elevationruler.settings.grid-terrain-choice-percent-area": "Percent Area", "elevationruler.settings.grid-terrain-choice-euclidean": "Euclidean", @@ -112,14 +116,20 @@ "elevationruler.settings.scale-text.name": "Scale Ruler Text", "elevationruler.settings.scale-text.hint": "When enabled, ruler text will be scaled depending on `canvas.dimensions.size`. Further adjust scaling by using `CONFIG.elevationruler.textScale`.", + "elevationruler.settings.force-grid-penalties.name": "Use Grid for Movement Penalties", + "elevationruler.settings.force-grid-penalties.hint": "When enabled, move penalties only apply if the token/drawing/region covers the center of the hex/square and penalties are applied per-square or per-hex on gridded maps. When disabled, movement penalties are applied proportionally to the move. For example, if a drawing with a x2 move penalty covers 3/4 of a grid square, it will apply to 3/4 of the move through that square (i.e., 1.5x penalty).", + "elevationruler.settings.customized-labels.name": "Customized Ruler Text", "elevationruler.settings.customized-labels.hint": "Customize the ruler text. Change styles using `CONFIG.elevationruler.labelStyles` and `CONFIG.elevationruler.labelIcons`.", "elevationruler.controls.prefer-token-elevation.name": "Prefer Token Elevation", "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. Movement under the drawing elevation will be ignored.", + "elevationruler.drawingconfig.movementPenalty.name": "Movement Penalty Percent", + "elevationruler.drawingconfig.movementPenalty.hint": "Set to 1 for no penalty, less than one to grant a bonus, greater than 1 to impose a penalty multiplier. For example, set to 2 to double movement cost through this area. Movement under the drawing elevation will be ignored.", + + "elevationruler.drawingconfig.flatMovementPenalty.name": "Use Flat Penalty", + "elevationruler.drawingconfig.flatMovementPenalty.hint": "If enabled, treats the penalty as a fixed amount of additional distance. For example, set to 5 to add +5 to each square of movement. Negative values provide a flat bonus.", "elevationruler.clearMovement": "Clear Combatant Movement", "elevationruler.waypoint": "waypoint", diff --git a/languages/it.json b/languages/it.json index 905d481..099dd85 100644 --- a/languages/it.json +++ b/languages/it.json @@ -58,6 +58,9 @@ "elevationruler.settings.token-ruler-combat-history.name": "Traccia movimenti di combattimento", "elevationruler.settings.token-ruler-combat-history.hint": "Per l'evidenziare la velocità dei token, somma tutte le mosse dei token durante il round quando abilitato.", + "elevationruler.settings.combine-prior-with-total.name": "Combina il movimento precedente con il movimento totale", + "elevationruler.settings.combine-prior-with-total.hint": "Quando è abilitato il tracciamento dei movimenti di combattimento, combina il movimento precedente del token nel round con il movimento totale. Altrimenti, posiziona il movimento precedente su una riga separata.", + "elevationruler.settings.token-speed-property.name": "Proprietà Camminare Token", "elevationruler.settings.token-speed-property.hint": "Per l'evidenziazione della velocità del token, questa è la proprietà che rappresenta la velocità di camminata del token.", @@ -88,6 +91,9 @@ "elevationruler.settings.pathfinding_limit_token_los.name": "Limita Trova Percorso alle Aree Esplorate", "elevationruler.settings.pathfinding_limit_token_los.hint": "Usando Trova Percorso, limita il raggio d'azione del percorso alle aree esplorate a meno che l'utente non sia il DM", + "elevationruler.settings.pathfinding_snap_to_grid.name": "Trova Percorso agganciato alla griglia", + "elevationruler.settings.pathfinding_snap_to_grid.hint": "Durante la ricerca del percorso su una mappa a griglia, aggancia il percorso ai centri della griglia a meno che ciò non provochi il blocco del percorso.", + "elevationruler.settings.grid-terrain-algorithm.name": "Misurazione Griglia Terreno", "elevationruler.settings.grid-terrain-algorithm.hint": "Quando sei su una griglia, come tenere conto delle penalità di movimento o dei bonus derivanti dal terreno e dai token? Centro: si applica se il terreno/token si sovrappone al centro della griglia; Area percentuale: si applica se il terreno/token copre almeno questa parte del quadrato/esagono della griglia; Euclideo: ripartizione proporzionale in base alla percentuale del segmento di linea all'interno del terreno/token tra questo quadrato/esagono della griglia e il precedente.", "elevationruler.settings.grid-terrain-choice-center-point": "Centro", @@ -100,11 +106,25 @@ "elevationruler.settings.automatic-movement-type.name": "Rilevamento automatico del tipo di movimento", "elevationruler.settings.automatic-movement-type.hint": "Rileva automaticamente il tipo di movimento in base alla posizione del token. Imposta l'effetto dello stato del token su 'volare' o 'scavare' per sovrascrivere.", + "elevationruler.settings.euclidean-grid-distance.name": "Preferisci la distanza euclidea", + "elevationruler.settings.euclidean-grid-distance.hint": "Per le scene a griglia, se viene scelto 'Esatto (√2)' per l'impostazione principale 'Diagonali griglia quadrata', ciò costringerà a misurare la distanza euclidea invece della distanza in unità esadecimali/quadrate. Per le griglie esagonali, viene influenzato solo il movimento di elevazione diagonale.", + + "elevationruler.settings.scale-text.name": "Testo Scala Righello", + "elevationruler.settings.scale-text.hint": "Se abilitato, il testo del righello verrà ridimensionato in base a 'canvas.dimensions.size'. Regola ulteriormente il ridimensionamento utilizzando `CONFIG.elevationruler.textScale`.", + + "elevationruler.settings.customized-labels.name": "Testo Righello Personalizzato", + "elevationruler.settings.customized-labels.hint": "Personalizza il testo del righello. Modifica gli stili utilizzando `CONFIG.elevationruler.labelStyles` e `CONFIG.elevationruler.labelIcons`.", + "elevationruler.controls.prefer-token-elevation.name": "Preferisci elevazione token", "elevationruler.controls.pathfinding-control.name": "Trova Percorso", "elevationruler.drawingconfig.movementPenalty.name": "Bonus/Penalità Movimento", "elevationruler.drawingconfig.movementPenalty.hint": "Impostato a 1 senza penalità. Valori maggiori di uno penalizzano il movimento di quella percentuale; valori inferiori a uno garantiscono effettivamente un bonus al movimento. Ad esempio, impostalo su 2 per raddoppiare il movimento in quest'area. Il movimento sotto l'elevazione del disegno verrà ignorato.", - "elevationruler.clearMovement": "Cancella Movimento Combattente" + "elevationruler.clearMovement": "Cancella Movimento Combattente", + "elevationruler.waypoint": "Punto di passaggio", + "elevationruler.up": "Su", + "elevationruler.down": "Giù", + "elevationruler.added": "Aggiunto", + "elevationruler.prior": "Precedente" } diff --git a/scripts/CombatTracker.js b/scripts/CombatTracker.js index 733ff0a..0aa484f 100644 --- a/scripts/CombatTracker.js +++ b/scripts/CombatTracker.js @@ -1,5 +1,6 @@ /* globals -game +game, +ui */ /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ @@ -10,7 +11,7 @@ import { TEMPLATES, MODULE_ID, FLAGS } from "./const.js"; export const PATCHES = {}; PATCHES.BASIC = {}; -import { injectConfiguration, renderTemplateSync } from "./util.js"; +import { renderTemplateSync } from "./util.js"; // ----- NOTE: Hooks ----- // @@ -31,15 +32,8 @@ function renderCombatTracker(app, html, data) { 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)); + html.find(`.${MODULE_ID}`).click(ev => clearMovement.call(app, ev)); } PATCHES.BASIC.HOOKS = { renderCombatTracker }; diff --git a/scripts/ModuleSettingsAbstract.js b/scripts/ModuleSettingsAbstract.js index 3a5a8e0..4cc7f0c 100644 --- a/scripts/ModuleSettingsAbstract.js +++ b/scripts/ModuleSettingsAbstract.js @@ -59,11 +59,11 @@ export class ModuleSettingsAbstract { const cached = this.cache.get(key); if ( typeof cached !== "undefined" ) { // For debugging, can confirm against what the value should be. -// const origValue = game.settings.get(MODULE_ID, key); -// if ( origValue !== cached ) { -// console.debug(`Settings cache fail: ${origValue} !== ${cached} for key ${key}`); -// return origValue; -// } + // const origValue = game.settings.get(MODULE_ID, key); + // if ( origValue !== cached ) { + // console.debug(`Settings cache fail: ${origValue} !== ${cached} for key ${key}`); + // return origValue; + // } return cached; diff --git a/scripts/Patcher.js b/scripts/Patcher.js index 41fc3ff..d68e6c5 100644 --- a/scripts/Patcher.js +++ b/scripts/Patcher.js @@ -197,7 +197,7 @@ export class Patcher { Object.defineProperty(cl, name, descriptor); const prototypeName = cl.constructor?.name; - const id = `${prototypeName ?? cl.name }.${prototypeName ? "prototype." : ""}${name}`; + const id = `${prototypeName ?? cl.name}.${prototypeName ? "prototype." : ""}${name}`; return { id, args: { cl, name } }; } @@ -401,7 +401,9 @@ export class MethodPatch extends AbstractPatch { else if ( this.config.isSetter ) this.prevMethod = this.prevMethod?.set; else this.prevMethod = this.prevMethod?.value; - this.regId = Patcher.addClassMethod(this.#cl, this.target, this.patchFn, { getter: this.config.isGetter, setter: this.config.isSetter }); + this.regId = Patcher.addClassMethod(this.#cl, this.target, this.patchFn, { + getter: this.config.isGetter, setter: this.config.isSetter + }); } /** @@ -413,7 +415,9 @@ export class MethodPatch extends AbstractPatch { // Add back the original, if any. if ( this.prevMethod ) { - Patcher.addClassMethod(this.#cl, this.target, this.prevMethod, { getter: this.config.isGetter, setter: this.config.isSetter }); + Patcher.addClassMethod(this.#cl, this.target, this.prevMethod, { + getter: this.config.isGetter, setter: this.config.isSetter + }); this.prevMethod = undefined; } this.regId = undefined; diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 606725f..ac96a5b 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -2,6 +2,7 @@ canvas, CONFIG, CONST, +foundry, game, PIXI, Ruler, @@ -276,7 +277,7 @@ function _getMeasurementDestination(wrapped, point, {snap=true}={}) { const origin = token.getCenterPoint(); const delta = origPoint.subtract(origin, PIXI.Point._tmp); let position = PIXI.Point._tmp2.copyFrom(token.document).add(delta, PIXI.Point._tmp2); - const tlSnapped = token._preview.getSnappedPosition(position); + const tlSnapped = token._preview.getSnappedPosition(position); return token.getCenterPoint(tlSnapped); } @@ -499,8 +500,7 @@ function _getCostFunction() { return (prevOffset, currOffset, offsetDistance) => { if ( !(prevOffset instanceof GridCoordinates3d) ) prevOffset = GridCoordinates3d.fromOffset(prevOffset); if ( !(currOffset instanceof GridCoordinates3d) ) currOffset = GridCoordinates3d.fromOffset(currOffset); - const penalty = movePenaltyInstance.movementPenaltyForSegment(prevOffset, currOffset); - return offsetDistance * penalty; + return movePenaltyInstance.movementCostForSegment(prevOffset, currOffset, offsetDistance); }; } @@ -519,9 +519,11 @@ function _getSegmentLabel(wrapped, segment) { } // Force distance to be between waypoints instead of (possibly pathfinding) segments. + // Use cost instead of straight distance for the label. const origSegmentDistance = segment.distance; const origTotalDistance = this.totalDistance; - segment.distance = roundMultiple(segment.waypoint.distance); + segment.distance = roundMultiple(segment.waypoint.cost); + this.totalDistance = this.totalCost; const origLabel = wrapped(segment); segment.distance = origSegmentDistance; this.totalDistance = origTotalDistance; diff --git a/scripts/Token.js b/scripts/Token.js index accd369..b7c7ac2 100644 --- a/scripts/Token.js +++ b/scripts/Token.js @@ -1,6 +1,7 @@ /* globals canvas, CanvasAnimation, +CONFIG, foundry, game, Ruler @@ -96,7 +97,7 @@ function updateToken(document, changed, _options, _userId) { const tokenHistory = token[MODULE_ID].measurementHistory ??= []; const gridUnitsToPixels = CONFIG.GeometryLib.utils.gridUnitsToPixels; const origin = token.getCenterPoint(document); - const dest = token.getCenterPoint({ x: changed.x ?? document.x, y: changed.y ?? document.y}) + const dest = token.getCenterPoint({ x: changed.x ?? document.x, y: changed.y ?? document.y}); origin.z = gridUnitsToPixels(document.elevation); origin.teleport = false; origin.cost = 0; @@ -129,7 +130,7 @@ function _onDragLeftCancel(wrapped, event) { // Add waypoint on right click const ruler = canvas.controls.ruler; - if ( event.button === 2 && ruler._isTokenRuler && ruler.active && ruler.state === Ruler.STATES.MEASURING ) { + if ( event.button === 2 && ruler._isTokenRuler && ruler.active && ruler.state === Ruler.STATES.MEASURING ) { log("Token#_onDragLeftMove|Token ruler active"); event.preventDefault(); if ( event.ctrlKey ) ruler._removeWaypoint(event.interactionData.origin, {snap: !event.shiftKey}); @@ -187,7 +188,7 @@ async function _onDragLeftDrop(wrapped, event) { return false; } - // ruler._state = Ruler.STATES.MOVING; // Do NOT set state to MOVING here in v12, as it will break the canvas. + // NO: ruler._state = Ruler.STATES.MOVING; // Do NOT set state to MOVING here in v12, as it will break the canvas. ruler._onMoveKeyDown(event); // Movement is async here but not awaited in _onMoveKeyDown. } @@ -259,4 +260,4 @@ function noStartEase(easing) { function noEndEase(easing) { if ( typeof easing === "string" ) easing = CanvasAnimation[easing]; return pt => (pt > 0.5) ? pt : easing(pt); -} \ No newline at end of file +} diff --git a/scripts/const.js b/scripts/const.js index 8c13765..2b3cc5c 100644 --- a/scripts/const.js +++ b/scripts/const.js @@ -16,6 +16,7 @@ export const TEMPLATES = { export const FLAGS = { MOVEMENT_SELECTION: "selectedMovementType", MOVEMENT_PENALTY: "movementPenalty", + MOVEMENT_PENALTY_FLAT: "flatMovementPenalty", SCENE: { BACKGROUND_ELEVATION: "backgroundElevation" }, @@ -47,7 +48,9 @@ export const MOVEMENT_TYPES = { * @param {number} groundElev Ground elevation in grid units * @returns {MOVEMENT_TYPE} */ - forCurrentElevation: function movementTypeForCurrentElevation(currElev, groundElev = 0) { return Math.sign(currElev - groundElev) + 1; } + forCurrentElevation: function(currElev, groundElev = 0) { + return Math.sign(currElev - groundElev) + 1; + } }; diff --git a/scripts/geometry b/scripts/geometry index ca4d31f..15bae42 160000 --- a/scripts/geometry +++ b/scripts/geometry @@ -1 +1 @@ -Subproject commit ca4d31f0266d32f52927fd7e752b2fd4477eb156 +Subproject commit 15bae42613c94f943c66eb51fe42b70a4a8f3266 diff --git a/scripts/measurement/Grid.js b/scripts/measurement/Grid.js index 9c13cdb..932d206 100644 --- a/scripts/measurement/Grid.js +++ b/scripts/measurement/Grid.js @@ -1,12 +1,15 @@ /* globals canvas, -CONST +CONFIG, +CONST, +game */ /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ "use strict"; import { GridCoordinates3d } from "../geometry/3d/GridCoordinates3d.js"; import { Point3d } from "../geometry/3d/Point3d.js"; +import { Settings } from "../settings.js"; /** * Modify Grid classes to measure in 3d. @@ -81,7 +84,7 @@ function directPath3dSquare(start, end, path2d) { prevOffset.i = path2d[0].i; prevOffset.j = path2d[0].j; - // currOffset will be modified in the loop; set to end to get elevation steps now. + // The currOffset will be modified in the loop; set to end to get elevation steps now. const currOffset = GridCoordinates3d.fromObject(end); // Do 1 elevation move for each 2d diagonal move. Spread out over the diagonal steps. @@ -99,12 +102,14 @@ function directPath3dSquare(start, end, path2d) { // Do 1 elevation move for each 2d non-diagonal move. Spread out over the non-diagonal steps. const num2dStraight = num2dMoves - num2dDiagonal; - let diagonalElevationStepsRemaining = Math.min(elevationStepsRemaining - doubleDiagonalElevationStepsRemaining, num2dStraight); + let diagonalElevationStepsRemaining = Math.min(elevationStepsRemaining + - doubleDiagonalElevationStepsRemaining, num2dStraight); let diagonalElevationStep = 0; const doDiagonalElevationStepMod = Math.ceil(num2dStraight / (diagonalElevationStepsRemaining + 1)); // Rest are all additional elevation-only moves. Spread out evenly. - let additionalElevationStepsRemaining = Math.max(0, elevationStepsRemaining - diagonalElevationStepsRemaining - diagonalElevationStepsRemaining); + let additionalElevationStepsRemaining = Math.max(0, + elevationStepsRemaining - diagonalElevationStepsRemaining - diagonalElevationStepsRemaining); const doAdditionalElevationStepMod = Math.ceil(num2dMoves / (additionalElevationStepsRemaining + 1)); currOffset.k = prevOffset.k; // Begin with the starting elevation, incrementing periodically in the loop. @@ -113,9 +118,13 @@ function directPath3dSquare(start, end, path2d) { currOffset.setOffset2d(path2d[i]); const is2dDiagonal = (currOffset.i !== prevOffset.i) && (currOffset.j !== prevOffset.j); - const doDoubleDiagonalElevationStep = is2dDiagonal && doubleDiagonalElevationStepsRemaining > 0 && ((doubleDiagonalElevationStep + 1) % doDoubleDiagonalElevationStepMod) === 0; - const doDiagonalElevationStep = !is2dDiagonal && diagonalElevationStepsRemaining > 0 && ((diagonalElevationStep + 1) % doDiagonalElevationStepMod) === 0; - const doAdditionalElevationSteps = additionalElevationStepsRemaining > 0 && ((i + 1) % doAdditionalElevationStepMod) === 0; + const doDoubleDiagonalElevationStep = is2dDiagonal + && doubleDiagonalElevationStepsRemaining > 0 + && ((doubleDiagonalElevationStep + 1) % doDoubleDiagonalElevationStepMod) === 0; + const doDiagonalElevationStep = !is2dDiagonal && diagonalElevationStepsRemaining > 0 + && ((diagonalElevationStep + 1) % doDiagonalElevationStepMod) === 0; + const doAdditionalElevationSteps = additionalElevationStepsRemaining > 0 + && ((i + 1) % doAdditionalElevationStepMod) === 0; // Either double or normal diagonals are the same but have separate tracking. if ( doDoubleDiagonalElevationStep ) { @@ -130,7 +139,7 @@ function directPath3dSquare(start, end, path2d) { path3d.push(currOffset.clone()); if ( doAdditionalElevationSteps ) { - let elevationSteps = Math.ceil(additionalElevationStepsRemaining / stepsRemaining); + let elevationSteps = Math.ceil(additionalElevationStepsRemaining / stepsRemaining); while ( elevationSteps > 0 ) { currOffset.k += 1; elevationSteps -= 1; @@ -143,9 +152,6 @@ function directPath3dSquare(start, end, path2d) { return path3d; } - - - /** * Construct a function to determine the offset cost for this canvas for a single 3d move on a square grid. * @param {number} numDiagonals @@ -161,7 +167,7 @@ function singleOffsetSquareDistanceFn(numDiagonals = 0) { if ( canvas.grid.diagonals === D.ALTERNATING_1 || canvas.grid.diagonals === D.ALTERNATING_2 ) { const kFn = canvas.grid.diagonals === D.ALTERNATING_1 ? () => nDiag & 1 ? 2 : 1 - : () => nDiag & 1 ? 1 : 2; + : () => nDiag & 1 ? 1 : 2; fn = (prevOffset, currOffset) => { const isElevationMove = prevOffset.k !== currOffset.k; const isStraight2dMove = (prevOffset.i === currOffset.i) ^ (prevOffset.j === currOffset.j); @@ -171,16 +177,16 @@ function singleOffsetSquareDistanceFn(numDiagonals = 0) { const d2 = isDiagonal2dMove && isElevationMove; if ( d1 || d2 ) nDiag++; const k = kFn(); - return (s + k * d1 + k * d2) * canvas.grid.distance; + return (s + (k * d1) + (k * d2)) * canvas.grid.distance; }; } else { let k = 1; let k2 = 1; switch ( canvas.grid.diagonals ) { - case D.EQUIDISTANT: k = 1; k2 = 1; break; - case D.EXACT: k = Math.SQRT2; k2 = Math.SQRT3; break; - case D.APPROXIMATE: k = 1.5; k2 = 1.75; break; - case D.RECTILINEAR: k = 2; k2 = 3; break; + case D.EQUIDISTANT: k = 1; k2 = 1; break; + case D.EXACT: k = Math.SQRT2; k2 = Math.SQRT3; break; + case D.APPROXIMATE: k = 1.5; k2 = 1.75; break; + case D.RECTILINEAR: k = 2; k2 = 3; break; } fn = (prevOffset, currOffset) => { const isElevationMove = prevOffset.k !== currOffset.k; @@ -189,11 +195,11 @@ function singleOffsetSquareDistanceFn(numDiagonals = 0) { const s = isStraight2dMove || (!isDiagonal2dMove && isElevationMove); const d1 = isDiagonal2dMove && !isElevationMove; const d2 = isDiagonal2dMove && isElevationMove; - return (s + k * d1 + k2 * d2) * canvas.grid.distance; + return (s + (k * d1) + (k2 * d2)) * canvas.grid.distance; }; } Object.defineProperty(fn, "diagonals", { - get : () => nDiag + get: () => nDiag }); return fn; } @@ -222,7 +228,7 @@ function directPath3dHex(start, end, path2d) { startOffset.i = path2d[0].i; startOffset.j = path2d[0].j; - // currOffset will be modified in the loop; set to end to get elevation steps now. + // The currOffset will be modified in the loop; set to end to get elevation steps now. const currOffset = GridCoordinates3d.fromObject(end); const path3d = [startOffset.clone()]; @@ -233,8 +239,9 @@ function directPath3dHex(start, end, path2d) { currOffset.setOffset2d(path2d[i]); const doElevationStep = ((i + 1) % doElevationStepMod) === 0; - let elevationSteps = doElevationStep && (elevationStepsRemaining > 0) ? Math.ceil(elevationStepsRemaining / stepsRemaining) : 0; - elevationStepsRemaining -= elevationSteps + let elevationSteps = doElevationStep + && (elevationStepsRemaining > 0) ? Math.ceil(elevationStepsRemaining / stepsRemaining) : 0; + elevationStepsRemaining -= elevationSteps; // Apply the first elevation step as a diagonal upwards move in combination with the canvas 2d move. if ( elevationSteps ) { @@ -269,7 +276,7 @@ function singleOffsetHexDistanceFn(numDiagonals = 0) { if ( canvas.grid.diagonals === D.ALTERNATING_1 || canvas.grid.diagonals === D.ALTERNATING_2 ) { const kFn = canvas.grid.diagonals === D.ALTERNATING_1 ? () => nDiag & 1 ? 2 : 1 - : () => nDiag & 1 ? 1 : 2; + : () => nDiag & 1 ? 1 : 2; fn = (prevOffset, currOffset) => { // For hex moves, no diagonal 2d. Just diagonal if both elevating and moving in 2d. const isElevationMove = prevOffset.k !== currOffset.k; @@ -278,30 +285,50 @@ function singleOffsetHexDistanceFn(numDiagonals = 0) { const d = !s; nDiag += d; const k = kFn(); - return (s + k * d) * canvas.grid.distance; + return (s + (k * d)) * canvas.grid.distance; }; } else { let k = 1; switch ( canvas.grid.diagonals ) { - case D.EQUIDISTANT: k = 1; break; - case D.EXACT: k = Math.SQRT2; break; - case D.APPROXIMATE: k = 1.5; break; - case D.RECTILINEAR: k = 2; break; + case D.EQUIDISTANT: k = 1; break; + case D.EXACT: k = Math.SQRT2; break; + case D.APPROXIMATE: k = 1.5; break; + case D.RECTILINEAR: k = 2; break; } fn = (prevOffset, currOffset) => { const isElevationMove = prevOffset.k !== currOffset.k; const is2dMove = prevOffset.i !== currOffset.i || prevOffset.j !== currOffset.j; const s = isElevationMove ^ is2dMove; const d = !s; - return (s + k * d) * canvas.grid.distance; + return (s + (k * d)) * canvas.grid.distance; }; } Object.defineProperty(fn, "diagonals", { - get : () => nDiag + get: () => nDiag }); return fn; } +/** + * Get the function to measure the offset distance for a given distance with given previous diagonals. + * @param {number} [diagonals=0] + * @returns {function} + */ +export function getOffsetDistanceFn(diagonals = 0) { + let offsetDistanceFn; + switch ( canvas.grid.type ) { + case CONST.GRID_TYPES.GRIDLESS: + offsetDistanceFn = (a, b) => CONFIG.GeometryLib.utils.pixelsToGridUnits(Point3d.distanceBetween(a, b)); + break; + case CONST.GRID_TYPES.SQUARE: + offsetDistanceFn = singleOffsetSquareDistanceFn(diagonals); + break; + default: // All hex grids + offsetDistanceFn = singleOffsetHexDistanceFn(diagonals); + } + return offsetDistanceFn; +} + /** * Measure a path for a gridded scene. Handles hex and square grids. * @param {GridMeasurePathWaypoint[]} waypoints The waypoints the path must pass through @@ -317,25 +344,18 @@ function _measurePath(wrapped, waypoints, { cost }, result) { result.segments.forEach(segment => initializeResultObject(segment)); // For each waypoint, project from 3d if the waypoint is a 3d class. - // The projected point can be used to determine distance but not movement cost because the passed coordinates will be incorrect. + // The projected point can be used to determine distance but not movement cost + // because the passed coordinates will be incorrect. // Movement cost requires knowing the 3d positions. // Cannot combine the projected waypoints to measure all at once, b/c they would be misaligned. // Copy the waypoint so it can be manipulated. - let diagonals = 0; let start = waypoints[0]; - let offsetDistanceFn; cost ??= (prevOffset, currOffset, offsetDistance) => offsetDistance; - switch ( canvas.grid.type ) { - case CONST.GRID_TYPES.GRIDLESS: - offsetDistanceFn = (a, b) => CONFIG.GeometryLib.utils.pixelsToGridUnits(Point3d.distanceBetween(a, b)); - break; - case CONST.GRID_TYPES.SQUARE: - offsetDistanceFn = singleOffsetSquareDistanceFn(diagonals); - break; - default: // All hex grids - offsetDistanceFn = singleOffsetHexDistanceFn(diagonals); - } + const offsetDistanceFn = getOffsetDistanceFn(0); // Diagonals = 0. const altGridDistanceFn = GridCoordinates3d.alternatingGridDistanceFn(); + let diagonals = canvas.grid.diagonals ?? game.settings.get("core", "gridDiagonals"); + const D = GridCoordinates3d.GRID_DIAGONALS; + if ( diagonals === D.EXACT && Settings.get(Settings.KEYS.MEASURING.EUCLIDEAN_GRID_DISTANCE) ) diagonals = D.EUCLIDEAN; for ( let i = 1, n = waypoints.length; i < n; i += 1 ) { const end = waypoints[i]; const path3d = canvas.grid.getDirectPath([start, end]); @@ -345,7 +365,7 @@ function _measurePath(wrapped, waypoints, { cost }, result) { const prevDiagonals = offsetDistanceFn.diagonals; for ( let j = 1, n = path3d.length; j < n; j += 1 ) { const currPathPt = path3d[j]; - const dist = GridCoordinates3d.gridDistanceBetween(prevPathPt, currPathPt, altGridDistanceFn); + const dist = GridCoordinates3d.gridDistanceBetween(prevPathPt, currPathPt, { altGridDistanceFn, diagonals }); const offsetDistance = offsetDistanceFn(prevPathPt, currPathPt); segment.distance += dist; segment.offsetDistance += offsetDistance; diff --git a/scripts/measurement/MovePenalty.js b/scripts/measurement/MovePenalty.js index 7c46749..4310c6a 100644 --- a/scripts/measurement/MovePenalty.js +++ b/scripts/measurement/MovePenalty.js @@ -11,6 +11,7 @@ import { MODULE_ID, FLAGS, MODULES_ACTIVE, SPEED, MOVEMENT_TYPES } from "../cons import { Settings } from "../settings.js"; import { movementType } from "../token_hud.js"; import { log, keyForValue } from "../util.js"; +import { getOffsetDistanceFn } from "./Grid.js"; /* Class to measure penalty, as percentage of distance, between two points. @@ -56,8 +57,9 @@ export class MovePenalty { */ constructor(moveToken, speedFn) { this.moveToken = moveToken; - this.speedFn = speedFn ?? (token => foundry.utils.getProperty(token, SPEED.ATTRIBUTES[keyForValue(MOVEMENT_TYPES, token.movementType)])); - this.localTokenClone = this.constructor._constructTokenClone(this.moveToken); + this.speedFn = speedFn ?? (token => + foundry.utils.getProperty(token, SPEED.ATTRIBUTES[keyForValue(MOVEMENT_TYPES, token.movementType)])); + this.#localTokenClone = this.constructor._constructTokenClone(this.moveToken); const tokenMultiplier = this.constructor.tokenMultiplier; const terrainAPI = this.constructor.terrainAPI; @@ -67,8 +69,9 @@ export class MovePenalty { if ( r.terrainmapper.hasTerrain ) this.regions.add(r); }); canvas.drawings.placeables.forEach(d => { - const penalty = d.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY); - if ( penalty && penalty !== 1 ) this.drawings.add(d) + const penalty = d.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY) ?? 1; + const useFlatPenalty = d.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY_FLAT); + if ( (!useFlatPenalty && penalty !== 1) || (useFlatPenalty && penalty !== 0) ) this.drawings.add(d); }); this.tokens.delete(moveToken); @@ -116,7 +119,7 @@ export class MovePenalty { * - @prop {TokenDocument} document * - @prop {Actor} actor */ - localTokenClone; + #localTokenClone; /** * Construct the local token clone. @@ -124,8 +127,8 @@ export class MovePenalty { * @returns {object} */ static _constructTokenClone(token) { - const actor = new CONFIG.Actor.documentClass(token.actor.toObject()) - const document = new CONFIG.Token.documentClass(token.document.toObject()) + const actor = new CONFIG.Actor.documentClass(token.actor.toObject()); + const document = new CONFIG.Token.documentClass(token.document.toObject()); const tClone = { document, actor, _original: token }; // Add the movementType and needed properties to calculate movement type. @@ -141,7 +144,7 @@ export class MovePenalty { }, elevationE: { get: function() { - return this.document.elevation + return this.document.elevation; } } }); @@ -155,35 +158,118 @@ export class MovePenalty { // ----- NOTE: Primary methods ----- // /** - * Determine the movement penalties along a start|end segment. - * @param {GridCoordinates3d} startCoords - * @param {GridCoordinates3d} endCoords - * @returns {number} The number used to multiply the move speed along the segment. + * Determine the movement cost for a segment. + * @param {GridCoordinates3d} startCoords Exact starting position + * @param {GridCoordinates3d} endCoords Exact ending position + * @param {number} costFreeDistance Measured distance of the segment (may be offset distance) + * @returns {number} The costFreeDistance + cost, in grid units. */ - movementPenaltyForSegment(startCoords, endCoords) { - const start = startCoords.center; - const end = endCoords.center; - const key = `${start.key}|${end.key}`; + movementCostForSegment(startCoords, endCoords, costFreeDistance = 0, forceGridPenalty) { // eslint-disable-line default-param-last + forceGridPenalty ??= Settings.get(Settings.KEYS.MEASURING.FORCE_GRID_PENALTIES); + forceGridPenalty &&= !canvas.grid.isGridless; + + // Did we already test this segment? + const startKey = forceGridPenalty ? startCoords.center.key : startCoords.key; + const endKey = forceGridPenalty ? endCoords.center.key : endCoords.key; + const key = `${startKey}|${endKey}`; if ( this.#penaltyCache.has(key) ) return this.#penaltyCache.get(key); - const t0 = performance.now(); - const cutawayIxs = this._cutawayIntersections(start, end); - if ( !cutawayIxs.length ) return 1; - const t1 = performance.now(); - const avgMultiplier = this._penaltiesForIntersections(start, end, cutawayIxs); - const t2 = performance.now(); - if ( CONFIG[MODULE_ID].debug ) { - console.group(`${MODULE_ID}|movementPenaltyForSegment`); - console.debug(`${startCoords.x},${startCoords.y},${startCoords.z}(${startCoords.i},${startCoords.j},${startCoords.k}) --> ${endCoords.x},${endCoords.y},${endCoords.z}(${endCoords.i},${endCoords.j},${endCoords.k})`); - console.table({ - _cutawayIntersections: (t1 - t0).toNearest(.01), - penaltiesForIntersections: (t2 - t1).toNearest(.01), - total: (t2 - t0).toNearest(.01) - }); - console.groupEnd(`${MODULE_ID}|movementPenaltyForSegment`); + let res = costFreeDistance; + if ( forceGridPenalty ) { + // Cost is assigned to each grid square/hex + const isOneStep = Math.abs(endCoords.i - startCoords.i) < 2 + && Math.abs(endCoords.j - startCoords.j) < 2 + && Math.abs(endCoords.k - startCoords.k) < 2; + if ( isOneStep ) return this.movementCostForGridSpace(endCoords, costFreeDistance); + + // Unlikely scenario where endCoords are more than 1 step away from startCoords. + let totalCost = 0; + const path = canvas.grid.getDirectPath([startCoords, endCoords]); + const offsetDistanceFn = getOffsetDistanceFn(); + let prevOffset = path[0]; + for ( let i = 1, n = path.length; i < n; i += 1 ) { + const currOffset = path[i]; + const offsetDist = offsetDistanceFn(prevOffset, currOffset); + totalCost += (this.movementCostForGridSpace(endCoords, offsetDist) - offsetDist); + prevOffset = currOffset; + } + res = totalCost + costFreeDistance; + } else { + // Cost is proportional to the distance of the segment covered by each penalty-imposing token,region,drawing. + const multiplier = this.proportionalCostForSegment(startCoords, endCoords); + res = costFreeDistance * multiplier; } - this.#penaltyCache.set(key, 1 / avgMultiplier); - return 1 / avgMultiplier; + this.#penaltyCache.set(key, res); + return res; + } + + + /** + * Determine the movement cost when in a specific grid space. + * Typically used with Settings.KEYS.FORCE_GRID_PENALTIES. + * @param {GridCoordinates3d} coords Exact starting position + * @param {number} costFreeDistance Measured distance of the step + * @returns {number} The additional cost, in grid units, plus the costFreeDistance. + */ + movementCostForGridSpace(coords, costFreeDistance = 0) { + // Determine what regions, tokens, drawings overlap the center point. + const centerPt = coords.center; + const regions = [...this.regions].filter(r => r.testPoint(centerPt, centerPt.elevation)); + const tokens = [...this.tokens].filter(t => t.constrainedTokenBorder.contains(centerPt.x, centerPt.y) + && centerPt.elevation.between(t.bottomE, t.topE)); + const drawings = [...this.drawings].filter(d => d.bounds.contains(centerPt.x, centerPt.y) + && d.elevationE <= centerPt.elevation); + + // Track all speed multipliers and flat penalties for the grid space. + let flatPenalty = 0; + let currentMultiplier = 1; + + // Drawings + drawings.forEach(d => { + const penalty = d.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY); + if ( d.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY_FLAT) ) flatPenalty += penalty; + else currentMultiplier *= penalty; + }); + + // Tokens + const tokenMultiplier = this.constructor.tokenMultiplier; + const useTokenFlat = this.constructor.useFlatTokenMultiplier; + if ( useTokenFlat ) flatPenalty += (tokenMultiplier * tokens.length); + else currentMultiplier *= (tokenMultiplier * tokens.length); + + // Regions + const testRegions = this.constructor.terrainAPI && regions.length; + const tClone = testRegions ? this.#initializeTokenClone() : this.moveToken; + const startingSpeed = this.speedFn(tClone) || 1; + regions.forEach(r => this.#addTerrainsToToken(tClone, r)); + + const speedInGrid = ((this.speedFn(tClone) || 1) * currentMultiplier); + const gridMult = startingSpeed / speedInGrid; + return (flatPenalty + (gridMult * costFreeDistance)); + + /* Example + Token has speed 30 and moves 10 grid units. + Assume speed is halved plus a +5 flat penalty. + 30 / 15 = 2 * 10 = 20 grid units + 5 penalty. + So instead of moving 10 units, it is as though the token moved 25. + */ + } + + + /** + * Determine the movement penalties along a start|end segment. + * By default, the penalty is apportioned based on the exact intersections of the penalty + * region to the segment. If `forceGridPenalty=true`, then the penalty is assigned per grid space. + * + * @param {GridCoordinates3d} startCoords Exact starting position + * @param {GridCoordinates3d} endCoords Exact ending position + * @returns {number} The number used to multiply the move speed along the segment. + */ + proportionalCostForSegment(startCoords, endCoords) { + // Intersections for each region, token, drawing. + const cutawayIxs = this._cutawayIntersections(startCoords, endCoords); + if ( !cutawayIxs.length ) return 1; + return this._penaltiesForIntersections(startCoords, endCoords, cutawayIxs); } // ----- NOTE: Secondary methods ----- // @@ -204,11 +290,12 @@ export class MovePenalty { */ _cutawayIntersections(start, end) { const cutawayIxs = []; - const terrainAPI = this.constructor.terrainAPI; - for ( const region of this.pathRegions ) { - const ixs = region.terrainmapper._cutawayIntersections(start, end); - ixs.forEach(ix => ix.region = region); - cutawayIxs.push(...ixs); + if ( this.constructor.terrainAPI ) { + for ( const region of this.pathRegions ) { + const ixs = region.terrainmapper._cutawayIntersections(start, end); + ixs.forEach(ix => ix.region = region); + cutawayIxs.push(...ixs); + } } for ( const token of this.pathTokens ) { const ixs = this.constructor.tokenCutawayIntersections(start, end, token); @@ -228,73 +315,89 @@ export class MovePenalty { * @param {Point3d} start * @param {Point3d} end * @param {CutawayIntersection[]} cutawayIxs - * @returns {CutawayIntersection[]} Intersections with penalties and intersections converted to distance for x axis. + * @returns {number} The penalty multiplier for the given start --> end */ _penaltiesForIntersections(start, end, cutawayIxs) { if ( !cutawayIxs.length ) return 1; - // Set up the token clone to add and subtract terrains. + // Tokens const tokenMultiplier = this.constructor.tokenMultiplier; - let tClone = this.moveToken; + const useTokenFlat = this.constructor.useFlatTokenMultiplier; + + // Regions const testRegions = this.constructor.terrainAPI && this.pathRegions; - if ( testRegions ) { - tClone = this.localTokenClone; - const Terrain = CONFIG.terrainmapper.Terrain; - const tokenTerrains = Terrain.allOnToken(tClone); - if ( tokenTerrains.length ) { - CONFIG.terrainmapper.Terrain.removeFromTokenLocally(tClone, tokenTerrains, { refresh: false }); - tClone.actor._initialize(); // This is slow; we really need something more specific to active effects. - } - } + const tClone = testRegions ? this.#initializeTokenClone() : this.moveToken; const startingSpeed = this.speedFn(tClone) || 1; // Traverse each intersection, determining the speed multiplier from starting speed // and calculating total time and distance. x meters / y meters/second = x/y seconds const { to2d, convertToDistance } = CONFIG.GeometryLib.utils.cutaway; let totalDistance = 0; + let totalUnmodifiedDistance = 0; let totalTime = 0; let currentMultiplier = 1; + let currentFlat = 0; const start2d = convertToDistance(to2d(start, start, end)); const end2d = convertToDistance(to2d(end, start, end)); let prevIx = start2d; - //const changePts = []; cutawayIxs = cutawayIxs.map(ix => convertToDistance(shallowCopyCutawayIntersection(ix))); // Avoid modifying the originals. cutawayIxs.push(end2d); cutawayIxs.sort((a, b) => a.x - b.x); for ( const ix of cutawayIxs ) { // Must invert the multiplier to apply them as penalties. So a 2x penalty is 1/2 times speed. const multFn = ix.movingInto ? x => 1 / x : x => x; + const addFn = ix.movingInto ? x => x : x => -x; const terrainFn = ix.movingInto ? this.#addTerrainsToToken.bind(this) : this.#removeTerrainsFromToken.bind(this); // Handle all intersections at the same point. if ( ix.almostEqual(prevIx) ) { - if ( ix.token ) currentMultiplier *= multFn(tokenMultiplier); - if ( ix.drawing ) currentMultiplier *= multFn(ix.drawing.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY)); + if ( ix.token ) { + if ( useTokenFlat ) currentFlat += addFn(tokenMultiplier); + else currentMultiplier *= multFn(tokenMultiplier); + } + if ( ix.drawing ) { + const penalty = ix.drawing.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY); + if ( ix.drawing.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY_FLAT) ) currentFlat += addFn(penalty); + else currentMultiplier *= multFn(penalty); + } if ( ix.region ) terrainFn(tClone, ix.region); continue; } // Now we have prevIx --> ix. + prevIx.flat = currentFlat; prevIx.multiplier = currentMultiplier; - prevIx.dist = PIXI.Point.distanceBetween(prevIx, ix); - prevIx.tokenSpeed = (this.speedFn(tClone) || 1) * prevIx.multiplier; + prevIx.dist = CONFIG.GeometryLib.utils.pixelsToGridUnits(PIXI.Point.distanceBetween(prevIx, ix)); + totalUnmodifiedDistance += prevIx.dist; + + // Speed is adjusted when moving through regions with a multiplier. + prevIx.tokenSpeed = ((this.speedFn(tClone) || 1) * prevIx.multiplier); + + // Flat adds extra distance to the grid square. Diagonal is longer, so will have larger penalty. + prevIx.dist += (prevIx.dist * currentFlat / canvas.grid.distance); totalDistance += prevIx.dist; totalTime += (prevIx.dist / prevIx.tokenSpeed); - //changePts.push(prevIx); prevIx = ix; if ( ix.almostEqual(end2d) ) break; // Account for the changes due to ix. - if ( ix.token ) currentMultiplier *= multFn(tokenMultiplier); - if ( ix.drawing ) currentMultiplier *= multFn(ix.drawing.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY)); + if ( ix.token ) { + if ( useTokenFlat ) currentFlat += addFn(tokenMultiplier); + else currentMultiplier *= multFn(tokenMultiplier); + } + if ( ix.drawing ) { + const penalty = ix.drawing.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY); + if ( ix.drawing.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_PENALTY_FLAT) ) currentFlat += addFn(penalty); + else currentMultiplier *= multFn(penalty); + } if ( ix.region ) terrainFn(tClone, ix.region); } // Determine the ratio compared to a set speed - const totalDefaultTime = totalDistance / startingSpeed; + const totalDefaultTime = totalUnmodifiedDistance / startingSpeed; const avgMultiplier = (totalDefaultTime / totalTime) || 0; - return avgMultiplier; + return 1 / avgMultiplier; } /** @@ -331,10 +434,28 @@ export class MovePenalty { log(`#removeTerrainsFromToken|\tremoveLocally: ${(t1 - t0).toNearest(0.01)} ms\tinitialize: ${(t2 - t1).toNearest(0.01)} ms`); } + /** + * Initialize the token clone for testing movement penalty through regions. + * @returns {object} Token like object + */ + #initializeTokenClone() { + const tClone = this.#localTokenClone; + const Terrain = CONFIG.terrainmapper.Terrain; + const tokenTerrains = Terrain.allOnToken(tClone); + if ( tokenTerrains.length ) { + CONFIG.terrainmapper.Terrain.removeFromTokenLocally(tClone, tokenTerrains, { refresh: false }); + tClone.actor._initialize(); // This is slow; we really need something more specific to active effects. + } + return tClone; + } + // ----- NOTE: Static getters ----- // /** @type {number} */ - static get tokenMultiplier() { return Settings.get(Settings.KEYS.TOKEN_RULER.TOKEN_MULTIPLIER); } + static get tokenMultiplier() { return Settings.get(Settings.KEYS.MEASURING.TOKEN_MULTIPLIER); } + + /** @type {boolean} */ + static get useFlatTokenMultiplier() { return Settings.get(Settings.KEYS.MEASURING.TOKEN_MULTIPLIER_FLAT); } /** @type {object|undefined} */ static get terrainAPI() { return MODULES_ACTIVE.API?.TERRAIN_MAPPER; } @@ -373,7 +494,6 @@ export class MovePenalty { } } - /** * Duplicate pertinent parts of a CutawayIntersection. * @param {CutawayIntersection} ix @@ -385,8 +505,6 @@ function shallowCopyCutawayIntersection(ix) { return newIx; } - - /** * A function that returns the cost for a given move between grid/gridless spaces. * In square and hexagonal grids the grid spaces are always adjacent unless teleported. diff --git a/scripts/module.js b/scripts/module.js index 2cf4a24..0ba38fc 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -105,7 +105,7 @@ Hooks.once("init", function() { * Settings related to the ruler text labels. */ labeling: { - /** + /** * Ruler label styles */ styles: { @@ -170,7 +170,7 @@ Hooks.once("init", function() { Settings }; - loadTemplates(Object.values(TEMPLATES)).then(_value => log(`Templates loaded.`)); + loadTemplates(Object.values(TEMPLATES)).then(_value => log("Templates loaded.")); }); // Setup is after init; before ready. diff --git a/scripts/patching.js b/scripts/patching.js index b860270..cfe73dd 100644 --- a/scripts/patching.js +++ b/scripts/patching.js @@ -32,12 +32,12 @@ const PATCHES = { ClientKeybindings: PATCHES_ClientKeybindings, ClientSettings: PATCHES_ClientSettings, CombatTracker: PATCHES_CombatTracker, - ["foundry.canvas.edges.CanvasEdges"]: PATCHES_CanvasEdges, + "foundry.canvas.edges.CanvasEdges": PATCHES_CanvasEdges, DrawingConfig: PATCHES_DrawingConfig, - ["foundry.grid.GridlessGrid"]: PATCHES_GridlessGrid, - ["foundry.grid.HexagonalGrid"]: PATCHES_HexagonalGrid, - ["foundry.grid.SquareGrid"]: PATCHES_SquareGrid, - ["CONFIG.Canvas.rulerClass"]: PATCHES_Ruler, + "foundry.grid.GridlessGrid": PATCHES_GridlessGrid, + "foundry.grid.HexagonalGrid": PATCHES_HexagonalGrid, + "foundry.grid.SquareGrid": PATCHES_SquareGrid, + "CONFIG.Canvas.rulerClass": PATCHES_Ruler, Token: mergeObject(mergeObject(PATCHES_Token, PATCHES_TokenPF), PATCHES_TokenHUD), Wall: PATCHES_Wall }; diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index e91d7f0..1b67ecd 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -360,7 +360,7 @@ export class WallTracerEdge extends GraphEdge { * Tested "live" and not cached so door or wall orientation changes need not be tracked. * @param {Wall} wall Wall to test * @param {Point} origin Measure wall blocking from perspective of this origin point. - * @param {number} [elevation=0] Elevation of the point or origin to test. + * @param {number} [elevation=0] Elevation of the point or origin to test, in pixel units. * @returns {boolean} */ static wallBlocks(wall, origin, elevation = 0) { @@ -379,7 +379,7 @@ export class WallTracerEdge extends GraphEdge { && side === wall.document.dir ) return false; // Test for wall height. - if ( !elevation.between(wall.bottomZ, wall.topZ) ) return false; + if ( !elevation.between(wall.bottomZ, wall.topZ, false) ) return false; return true; } diff --git a/scripts/segment_labels_highlighting.js b/scripts/segment_labels_highlighting.js index 0d414cc..a4bf59f 100644 --- a/scripts/segment_labels_highlighting.js +++ b/scripts/segment_labels_highlighting.js @@ -93,7 +93,7 @@ export function segmentElevationLabel(ruler, segment) { if ( displayTotalChange ) { const segmentArrow = (elevationDelta > 0) ? "↑" :"↓"; let totalChange = `[${segmentArrow}${Math.abs(roundMultiple(elevationDelta))}`; - totalChange += (units ? ` ${units}]` : `]`); + totalChange += (units ? ` ${units}]` : "]"); labelParts.push(totalChange); } segment.label.style.align = segment.last ? "center" : "right"; @@ -137,13 +137,15 @@ function elevationForRulerLabel(ruler, segment) { export function segmentTerrainLabel(s) { if ( s.waypoint.cost.almostEqual(s.waypoint.offsetDistance) ) return ""; const units = (canvas.scene.grid.units) ? ` ${canvas.scene.grid.units}` : ""; - const moveDistance = roundMultiple(s.waypoint.cost); + const addedCost = roundMultiple(s.waypoint.cost - s.waypoint.offsetDistance); + const symbol = addedCost > 0 ? "+" : "-"; + if ( CONFIG[MODULE_ID].SPEED.useFontAwesome ) { const style = s.label.style; if ( !style.fontFamily.includes("fontAwesome") ) style.fontFamily += ",fontAwesome"; - return `\n${CONFIG[MODULE_ID].SPEED.terrainSymbol} ${moveDistance}${units}`; + return `\n${CONFIG[MODULE_ID].SPEED.terrainSymbol} ${symbol}${Math.abs(addedCost)}${units}`; } - return `\n${CONFIG[MODULE_ID].SPEED.terrainSymbol} ${moveDistance}${units}`; + return `\n${CONFIG[MODULE_ID].SPEED.terrainSymbol} ${symbol}${Math.abs(addedCost)}${units}`; } @@ -202,7 +204,7 @@ export function customizedTextLabel(ruler, segment, origLabel = "") { const childLabels = {}; // (1) Total Distance - let totalDistLabel = segment.last ? `${roundMultiple(ruler.totalDistance)}` : `${labelIcons.waypoint} ${roundMultiple(segment.waypoint.distance)}`; + let totalDistLabel = segment.last ? `${roundMultiple(ruler.totalCost)}` : `${labelIcons.waypoint} ${roundMultiple(segment.waypoint.cost)}`; // (2) Extra text // Strip out any custom text from the original label. @@ -212,7 +214,7 @@ export function customizedTextLabel(ruler, segment, origLabel = "") { // (3) Waypoint if ( segment.last && segment.waypoint.idx > 0 ) childLabels.waypoint = { icon: `${labelIcons.waypoint}`, - value: segment.waypoint.distance, + value: segment.waypoint.cost, descriptor: game.i18n.localize(`${MODULE_ID}.waypoint`) }; @@ -240,7 +242,8 @@ export function customizedTextLabel(ruler, segment, origLabel = "") { descriptor: game.i18n.localize(`${MODULE_ID}.added`) }; - // Align so that the icon is left justified and the value is right justified. This aligns the units label or descriptor. + // Align so that the icon is left justified and the value is right justified. + // This aligns the units label or descriptor. alignLeftAndRight(childLabels); // Build the string for each. @@ -313,7 +316,7 @@ function getDefaultLabel(segment) { function alignChildTextLeft(parent, child, priorChildren = []) { parent.anchor = { x: 0.5, y: 0.5 }; - child.anchor = { x: 0.5, y: 0.5 } + child.anchor = { x: 0.5, y: 0.5 }; /* Align relative to center of parent and child. -----•----- 11 @@ -337,7 +340,7 @@ function alignChildTextLeft(parent, child, priorChildren = []) { */ const SPACER = "\u200A"; // See https://unicode-explorer.com/articles/space-characters. function alignLeftAndRight(childLabels) { - const labelStyles = CONFIG[MODULE_ID].labeling.styles; + const labelStyles = CONFIG[MODULE_ID].labeling.styles; let targetWidth = 0; Object.entries(childLabels).forEach(([name, obj]) => { obj.iconValueStr = `${obj.icon} ${roundMultiple(obj.value)}`; diff --git a/scripts/segments.js b/scripts/segments.js index ba6e09c..7a313de 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -94,7 +94,12 @@ export function constructPathfindingSegments(segments, segmentMap) { for ( let i = 1; i < nPoints; i += 1 ) { const currPt = pathPoints[i]; currPt.z ??= A.z; - const newSegment = { ray: new Ray3d(prevPt, currPt), waypoint: {}, history: segment.history, teleport: segment.teleport }; + const newSegment = { + ray: new Ray3d(prevPt, currPt), + waypoint: {}, + history: segment.history, + teleport: segment.teleport + }; newSegment.waypoint.idx = segment.waypoint.idx; newSegments.push(newSegment); prevPt = currPt; @@ -126,7 +131,6 @@ export function elevateSegments(ruler, segments) { // Add destination as the fi const nHistory = ruler.history.length; for ( let i = 0, n = segments.length; i < n; i += 1 ) { const segment = segments[i]; - // segment.first = i === 0; segment.waypoint = { idx: Math.max(i - nHistory, -1) }; } @@ -137,7 +141,7 @@ export function elevateSegments(ruler, segments) { // Add destination as the fi _userElevationIncrements: 0, _forceToGround: Settings.FORCE_TO_GROUND, elevation: ruler.destinationElevation - } + }; const waypoints = [...ruler.waypoints, destWaypoint]; for ( const segment of segments ) { if ( segment.history ) { diff --git a/scripts/settings.js b/scripts/settings.js index eea3ae7..2076fb7 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -50,13 +50,16 @@ const SETTINGS = { EUCLIDEAN_GRID_DISTANCE: "euclidean-grid-distance", AUTO_MOVEMENT_TYPE: "automatic-movement-type", COMBAT_HISTORY: "token-ruler-combat-history", + FORCE_GRID_PENALTIES: "force-grid-penalties", + TOKEN_MULTIPLIER: "token-terrain-multiplier", + TOKEN_MULTIPLIER_FLAT: "token-terrain-multiplier-flat" }, NO_MODS: "no-modules-message", TOKEN_RULER: { ENABLED: "enable-token-ruler", HIDE_GM: "hide-gm-ruler", - TOKEN_MULTIPLIER: "token-terrain-multiplier" + }, SPEED_HIGHLIGHTING: { @@ -70,15 +73,15 @@ const SETTINGS = { } }, -// GRID_TERRAIN: { -// ALGORITHM: "grid-terrain-algorithm", -// CHOICES: { -// CENTER: "grid-terrain-choice-center-point", -// PERCENT: "grid-terrain-choice-percent-area", -// EUCLIDEAN: "grid-terrain-choice-euclidean" -// }, -// AREA_THRESHOLD: "grid-terrain-area-threshold" -// }, + // GRID_TERRAIN: { + // ALGORITHM: "grid-terrain-algorithm", + // CHOICES: { + // CENTER: "grid-terrain-choice-center-point", + // PERCENT: "grid-terrain-choice-percent-area", + // EUCLIDEAN: "grid-terrain-choice-euclidean" + // }, + // AREA_THRESHOLD: "grid-terrain-area-threshold" + // }, }; @@ -309,48 +312,62 @@ export class Settings extends ModuleSettingsAbstract { type: Number }); - register(KEYS.TOKEN_RULER.TOKEN_MULTIPLIER, { - name: localize(`${KEYS.TOKEN_RULER.TOKEN_MULTIPLIER}.name`), - hint: localize(`${KEYS.TOKEN_RULER.TOKEN_MULTIPLIER}.hint`), + register(KEYS.MEASURING.TOKEN_MULTIPLIER, { + name: localize(`${KEYS.MEASURING.TOKEN_MULTIPLIER}.name`), + hint: localize(`${KEYS.MEASURING.TOKEN_MULTIPLIER}.hint`), scope: "world", config: true, default: 1, - type: Number, - range: { - max: 10, - min: 0, - step: 0.1 - } + type: Number + }); + + register(KEYS.MEASURING.TOKEN_MULTIPLIER_FLAT, { + name: localize(`${KEYS.MEASURING.TOKEN_MULTIPLIER_FLAT}.name`), + hint: localize(`${KEYS.MEASURING.TOKEN_MULTIPLIER_FLAT}.hint`), + scope: "world", + config: true, + default: true, + type: Boolean + }); + + register(KEYS.MEASURING.FORCE_GRID_PENALTIES, { + name: localize(`${KEYS.MEASURING.FORCE_GRID_PENALTIES}.name`), + hint: localize(`${KEYS.MEASURING.FORCE_GRID_PENALTIES}.hint`), + scope: "user", + config: true, + default: true, + type: Boolean, + requiresReload: false }); // ----- NOTE: Grid Terrain Measurement ----- // -// register(KEYS.GRID_TERRAIN.ALGORITHM, { -// name: localize(`${KEYS.GRID_TERRAIN.ALGORITHM}.name`), -// hint: localize(`${KEYS.GRID_TERRAIN.ALGORITHM}.hint`), -// scope: "world", -// config: true, -// default: KEYS.GRID_TERRAIN.CHOICES.CENTER, -// type: String, -// choices: { -// [KEYS.GRID_TERRAIN.CHOICES.CENTER]: localize(`${KEYS.GRID_TERRAIN.CHOICES.CENTER}`), -// [KEYS.GRID_TERRAIN.CHOICES.PERCENT]: localize(`${KEYS.GRID_TERRAIN.CHOICES.PERCENT}`), -// [KEYS.GRID_TERRAIN.CHOICES.EUCLIDEAN]: localize(`${KEYS.GRID_TERRAIN.CHOICES.EUCLIDEAN}`) -// } -// }); -// -// register(KEYS.GRID_TERRAIN.AREA_THRESHOLD, { -// name: localize(`${KEYS.GRID_TERRAIN.AREA_THRESHOLD}.name`), -// hint: localize(`${KEYS.GRID_TERRAIN.AREA_THRESHOLD}.hint`), -// scope: "world", -// config: true, -// default: 0.5, -// type: Number, -// range: { -// min: 0.1, -// max: 1, -// step: 0.1 -// } -// }); + // register(KEYS.GRID_TERRAIN.ALGORITHM, { + // name: localize(`${KEYS.GRID_TERRAIN.ALGORITHM}.name`), + // hint: localize(`${KEYS.GRID_TERRAIN.ALGORITHM}.hint`), + // scope: "world", + // config: true, + // default: KEYS.GRID_TERRAIN.CHOICES.CENTER, + // type: String, + // choices: { + // [KEYS.GRID_TERRAIN.CHOICES.CENTER]: localize(`${KEYS.GRID_TERRAIN.CHOICES.CENTER}`), + // [KEYS.GRID_TERRAIN.CHOICES.PERCENT]: localize(`${KEYS.GRID_TERRAIN.CHOICES.PERCENT}`), + // [KEYS.GRID_TERRAIN.CHOICES.EUCLIDEAN]: localize(`${KEYS.GRID_TERRAIN.CHOICES.EUCLIDEAN}`) + // } + // }); + // + // register(KEYS.GRID_TERRAIN.AREA_THRESHOLD, { + // name: localize(`${KEYS.GRID_TERRAIN.AREA_THRESHOLD}.name`), + // hint: localize(`${KEYS.GRID_TERRAIN.AREA_THRESHOLD}.hint`), + // scope: "world", + // config: true, + // default: 0.5, + // type: Number, + // range: { + // min: 0.1, + // max: 1, + // step: 0.1 + // } + // }); } static registerKeybindings() { @@ -405,7 +422,7 @@ export class Settings extends ModuleSettingsAbstract { { key: "Equal" } ], onDown: context => { - if ( canvas.controls?.ruler._isTokenRuler ) toggleTokenRulerWaypoint(context, true); + if ( canvas.controls?.ruler._isTokenRuler ) toggleTokenRulerWaypoint(context, true); }, precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL }); @@ -447,7 +464,7 @@ export class Settings extends ModuleSettingsAbstract { editable: [ { key: "KeyG" } ], - onDown: _context => { // eslint-disable-line no-unused-vars + onDown: _context => { const ruler = canvas.controls.ruler; if ( !ruler.active ) return; this.FORCE_TO_GROUND = !this.FORCE_TO_GROUND; @@ -491,7 +508,7 @@ export class Settings extends ModuleSettingsAbstract { const t2 = performance.now(); console.group(`${MODULE_ID}|Initialized scene graph and pathfinding.`); - console.debug(`${MODULE_ID}|Constructed scene graph in ${t1 - t0} ms.`) + console.debug(`${MODULE_ID}|Constructed scene graph in ${t1 - t0} ms.`); console.debug(`${MODULE_ID}|Tracked ${SCENE_GRAPH.wallIds.size} walls.`); console.debug(`Tracked ${SCENE_GRAPH.tokenIds.size} tokens.`); console.debug(`Located ${SCENE_GRAPH.edges.size} distinct edges.`); @@ -523,7 +540,7 @@ export class Settings extends ModuleSettingsAbstract { Pathfinder.dirty = true; const res = SCENE_GRAPH._checkInternalConsistency(); if ( !res.allConsistent ) { - log(`WallTracer|setTokenBlocksPathfinding ${document.id} resulted in inconsistent graph.`, SCENE_GRAPH, res); + log("WallTracer|setTokenBlocksPathfinding resulted in inconsistent graph.", SCENE_GRAPH, res); SCENE_GRAPH._reset(); } } diff --git a/scripts/system_attributes.js b/scripts/system_attributes.js index ec3f9e2..3f52c4b 100644 --- a/scripts/system_attributes.js +++ b/scripts/system_attributes.js @@ -37,7 +37,7 @@ const MaximumSpeedCategory = { name: "Maximum", color: Color.from(0xff0000), multiplier: Number.POSITIVE_INFINITY -} +}; Hooks.once("init", function() { // Set the default speed parameters for the given system. diff --git a/scripts/terrain_elevation.js b/scripts/terrain_elevation.js index 2db488a..094124d 100644 --- a/scripts/terrain_elevation.js +++ b/scripts/terrain_elevation.js @@ -18,7 +18,8 @@ Path endpoint is adjusted such that it goes up/down. Elevation increments here are relative to ground position. To avoid recursion and infinite loops: - Get the ground at the destination based on starting elevation ± user increments. - Measure a path from start to destination at the ending elevation. May or may not be at the end elevation. -- Each waypoint has a set elevation, based on its measured path. Only the destination waypoint can change, and only it is measured. +- Each waypoint has a set elevation, based on its measured path. + Only the destination waypoint can change, and only it is measured. Ruler Foundry default display: @@ -76,13 +77,8 @@ segment 0: • moveDistance • numDiagonal • numPrevDiagonalf - - */ - - - /* When dragging tokens Origin: each token elevation Other: terrain elevation for each token @@ -185,13 +181,16 @@ export function elevationFromWaypoint(waypoint, location, token) { // For normal ruler, if hovering over a token, use that token's elevation. // Use the maximum token elevation unless terrain is above us (e.g., tile above). - if ( !Settings.FORCE_TO_GROUND ) maxTokenE = maxTokenElevationAtLocation(location, terrainE > waypoint.elevation ? terrainE : undefined); + if ( !Settings.FORCE_TO_GROUND ) { + maxTokenE = maxTokenElevationAtLocation(location, terrainE > waypoint.elevation ? terrainE : undefined); + } if ( maxTokenE ) locationElevation = maxTokenE; // If the starting elevation is on the ground or force-to-ground is enabled, use the ground elevation. else locationElevation = elevationAtLocation(location, { startE: waypoint.elevation, - forceToGround: Settings.FORCE_TO_GROUND || waypoint.elevation.almostEqual(terrainElevationAtLocation(waypoint, waypoint.elevation)) + forceToGround: Settings.FORCE_TO_GROUND + || waypoint.elevation.almostEqual(terrainElevationAtLocation(waypoint, waypoint.elevation)) }); } else locationElevation = tokenElevationForMovement(waypoint, location, { token, @@ -205,7 +204,8 @@ export function elevationFromWaypoint(waypoint, location, token) { * @param {Point} location Location for which elevation is desired * @param {number} startE Elevation at the starting point * @param {object} [opts] - * @param {boolean} [opts.forceToGround=false] If true, override the end elevation with nearest ground to that 3d point. + * @param {boolean} [opts.forceToGround=false] If true, override the end elevation with + * nearest ground to that 3d point * @returns {number} The destination elevation, in grid units */ function elevationAtLocation(location, { startE = 0, forceToGround = false } ) { @@ -218,7 +218,8 @@ function elevationAtLocation(location, { startE = 0, forceToGround = false } ) { * @param {RegionMovementWaypoint} start Start location with elevation property * @param {Point} location Desired end location * @param {object} [opts] - * @param {boolean} [opts.forceToGround=false] If true, override the end elevation with nearest ground to that 3d point. + * @param {boolean} [opts.forceToGround=false] If true, override the end elevation + * with nearest ground to that 3d point * @returns {number} The destination elevation, in grid units */ function tokenElevationForMovement(start, location, opts = {}) { @@ -310,7 +311,7 @@ function retrieveVisibleTokens() { * @returns {Number|undefined} Point elevation or null if module not active or no region at location. */ function TMElevationAtPoint(location, startingElevation = Number.POSITIVE_INFINITY) { - const api = MODULES_ACTIVE.API.TERRAIN_MAPPER + const api = MODULES_ACTIVE.API.TERRAIN_MAPPER; if ( !api || !api.ElevationHandler ) return undefined; const waypoint = { ...location, elevation: startingElevation }; const res = api.ElevationHandler.nearestGroundElevation(waypoint); @@ -326,7 +327,7 @@ function TMElevationAtPoint(location, startingElevation = Number.POSITIVE_INFINI * @returns {number|undefined} Elevation, in grid units */ function TMElevationForMovement(start, end, opts) { - const api = MODULES_ACTIVE.API.TERRAIN_MAPPER + const api = MODULES_ACTIVE.API.TERRAIN_MAPPER; if ( !api || !api.ElevationHandler ) return undefined; return TMPathForMovement(start, end, opts).at(-1)?.elevation; } @@ -342,7 +343,7 @@ function TMElevationForMovement(start, end, opts) { function TMPathForMovement(start, end, opts) { start.elevation ??= CONFIG.GeometryLib.utils.pixelsToGridUnits(start.z); end.elevation ??= CONFIG.GeometryLib.utils.pixelsToGridUnits(end.z); - const api = MODULES_ACTIVE.API.TERRAIN_MAPPER + const api = MODULES_ACTIVE.API.TERRAIN_MAPPER; if ( !api || !api.ElevationHandler ) return [start, end]; return api.ElevationHandler.constructPath(start, end, opts); } @@ -387,7 +388,7 @@ export function LevelsElevationAtPoint(p, startingElevation = 0) { */ function levelsTilesAtPoint({x, y}) { const bounds = new PIXI.Rectangle(x, y, 1, 1); - const collisionTest = (o, rect) => { // eslint-disable-line no-unused-vars + const collisionTest = (o, rect) => { // The object o constains n (Quadtree node), r (rect), t (object to test) const flags = o.t.document?.flags?.levels; if ( !flags ) return false; diff --git a/scripts/token_hud.js b/scripts/token_hud.js index 7ced485..3ffcaa0 100644 --- a/scripts/token_hud.js +++ b/scripts/token_hud.js @@ -1,5 +1,6 @@ /* globals CONFIG, +document, game */ "use strict"; diff --git a/scripts/token_speed.js b/scripts/token_speed.js index 424b127..47cdb10 100644 --- a/scripts/token_speed.js +++ b/scripts/token_speed.js @@ -1,5 +1,6 @@ /* globals canvas, +CONFIG, CONST, game, PIXI @@ -54,7 +55,7 @@ export function tokenSpeedSegmentSplitter(ruler, token) { } const processed = []; - const unprocessed = [segment] + const unprocessed = [segment]; while ( (segment = unprocessed.pop()) ) { // Skip speed categories that do not provide a distance larger than the last. while ( speedCategory && maxDistance <= minDistance ) { @@ -160,7 +161,7 @@ function targetSplitForSegment(targetCost, a, b, numPrevDiagonal = 0) { // Use pixel (integer) steps so the points are at integer bounds. const MAX_ITER = 100; let bestDist = 0; - let step = Math.floor(totalDist) + let step = Math.floor(totalDist); let iter = 0; while ( step > 1 && iter < MAX_ITER) { iter += 1; @@ -221,7 +222,7 @@ function _splitSegmentAt(segment, breakpoint, numPrevDiagonal = 0) { s0.cumulativeDistance = segment.cumulativeDistance - s1.distance; s0.cumulativeOffsetDistance = segment.cumulativeOffsetDistance - s1.offsetDistance; - // s1 waypoint should equal the segment waypoint. + // The s1 waypoint should equal the segment waypoint. s0.waypoint.distance = segment.waypoint.distance - s1.distance; s0.waypoint.offsetDistance = segment.waypoint.offsetDistance - s1.offsetDistance; s0.waypoint.cost = segment.waypoint.cost - s1.cost; diff --git a/scripts/util.js b/scripts/util.js index 72c63ae..5251c2d 100644 --- a/scripts/util.js +++ b/scripts/util.js @@ -2,6 +2,7 @@ canvas CONFIG, CONST, +Handlebars, PIXI, renderTemplate */ @@ -13,7 +14,7 @@ import { Settings } from "./settings.js"; export function log(...args) { try { if ( CONFIG[MODULE_ID].debug ) console.debug(MODULE_ID, "|", ...args); - } catch(e) { // eslint-disable-line no-unused-vars + } catch(e) { // Empty } } @@ -23,7 +24,7 @@ export function log(...args) { * @param {number} n * @returns {boolean} */ -export function isEven(n) { return ~n & 1; } +export function isEven(n) { return ~n & 1; } /** * Is this number odd? @@ -240,7 +241,6 @@ export function * iterateGridUnderLine(origin, destination, { reverse = false } const offset = canvas.grid.getOffset({x, y}); const r1 = offset.i; const c1 = offset.j; - // const [r1, c1] = canvas.grid.grid.getGridPositionFromPixels(x, y); if ( r0 === r1 && c0 === c1 ) continue; // Skip the first one @@ -250,8 +250,6 @@ export function * iterateGridUnderLine(origin, destination, { reverse = false } const {x: xh, y: yh} = origin.projectToward(destination, th); const hOffset = canvas.grid.getOffset({ x: xh, y: yh }); yield [hOffset.i, hOffset.j]; - - // yield canvas.grid.grid.getGridPositionFromPixels(xh, yh); // [rh, ch] } // After so the halfway point is done first. @@ -285,7 +283,7 @@ export function renderTemplateSync(path, data) { * @param {number} num The number to round * @returns {number} The rounded number */ -export function roundMultiple (num) { +export function roundMultiple(num) { const multiple = Settings.get(Settings.KEYS.LABELING.ROUND_TO_MULTIPLE); if (multiple) return num.toNearest(multiple); return num; diff --git a/templates/drawing-config.html b/templates/drawing-config.html index 2eaef75..25f7b31 100644 --- a/templates/drawing-config.html +++ b/templates/drawing-config.html @@ -4,10 +4,18 @@
- {{rangePicker name="flags.elevationruler.movementPenalty" value=object.flags.elevationruler.movementPenalty step=0.1 min=0.1 max=10}} +

{{ localize "elevationruler.drawingconfig.movementPenalty.hint" }}

+
+ +
+ +
+

{{ localize "elevationruler.drawingconfig.flatMovementPenalty.hint" }}

+
+ \ No newline at end of file