From e13e5c477922adb90f437b91256a8652a0c45552 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Wed, 26 Jun 2024 16:59:47 -0700 Subject: [PATCH 01/23] =?UTF-8?q?=F0=9F=92=A1=20refactor|SpeedHighlighting?= =?UTF-8?q?|WIP=20to=20move=20speed=20fns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move to _highlightSegment, which would not require splitting the measurement segments for the entire ruler. Instead, just temporarily split the segments at the end. Need to revisit the movementType first. --- scripts/Ruler.js | 183 +--------------------------------------- scripts/segments.js | 77 ++++++++++------- scripts/token_speed.js | 184 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 209 deletions(-) create mode 100644 scripts/token_speed.js diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 1b406c8..98a98a9 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -1,7 +1,6 @@ /* globals canvas, CONFIG, -CONST, game, PIXI, Ruler, @@ -17,7 +16,6 @@ PATCHES.SPEED_HIGHLIGHTING = {}; import { SPEED, MODULE_ID, FLAGS } from "./const.js"; import { Settings } from "./settings.js"; import { Ray3d } from "./geometry/3d/Ray3d.js"; -import { Point3d } from "./geometry/3d/Point3d.js"; import { elevationFromWaypoint, elevationAtWaypoint, @@ -40,8 +38,6 @@ import { PhysicalDistance } from "./PhysicalDistance.js"; import { MoveDistance } from "./MoveDistance.js"; -import { gridShape, pointFromGridCoordinates, canvasElevationFromCoordinates } from "./grid_coordinates.js"; - /** * Modified Ruler * Measure elevation change at each waypoint and destination. @@ -274,7 +270,6 @@ function _computeDistance() { // Determine the distance of each segment. _computeSegmentDistances.call(this); - _computeTokenSpeed.call(this); // Always compute speed if there is a token b/c other users may get to see the speed. if ( debug ) { switch ( this.segments.length ) { @@ -334,7 +329,7 @@ function _computeSegmentDistances() { this.segments.at(-1).last = true; } for ( const segment of this.segments ) { - numPrevDiagonal = _measureSegment(segment, token, numPrevDiagonal); + numPrevDiagonal = measureSegment(segment, token, numPrevDiagonal); totalDistance += segment.distance; totalMoveDistance += segment.moveDistance; } @@ -351,7 +346,7 @@ function _computeSegmentDistances() { * @param {number} [numPrevDiagonal=0] Number of previous diagonals for the segment * @returns {number} numPrevDiagonal */ -function _measureSegment(segment, token, numPrevDiagonal = 0) { +export function measureSegment(segment, token, numPrevDiagonal = 0) { segment.numPrevDiagonal = numPrevDiagonal; const res = MoveDistance.measure( segment.ray.A, @@ -370,179 +365,6 @@ function _measureSegment(segment, token, numPrevDiagonal = 0) { // Also need to handle array of speed points. // Need CONFIG function that takes a token and gives array of speeds with colors. -/** - * Incrementally add together all segments. Split segment(s) at SpeedCategory maximum distances. - * Mark each segment with the distance, move distance, and SpeedCategory name. - * Does not assume segments have measurements, and modifies existing measurements. - * Segments modified in place. - */ -function _computeTokenSpeed() { - // Requires a movement token and a defined token speed. - const token = this.token; - if ( !token ) return; - - // Precalculate the token speed. - const tokenSpeed = SPEED.tokenSpeed(token); - if ( !tokenSpeed ) return; - - // Other constants - const gridless = canvas.grid.type === CONST.GRID_TYPES.GRIDLESS; - - // Variables changed in the loop - let totalDistance = 0; - let totalMoveDistance = 0; - let totalCombatMoveDistance = 0; - let minDistance = 0; - let numPrevDiagonal = 0; - let s = 0; - let segment; - - // Debugging - if ( CONFIG[MODULE_ID].debug ) { - if ( this.segments[0].moveDistance > 25 ) log(`${this.segments[0].moveDistance}`); - if ( this.segments[0].moveDistance > 30 ) log(`${this.segments[0].moveDistance}`); - if ( this.segments[0].moveDistance > 50 ) log(`${this.segments[0].moveDistance}`); - if ( this.segments[0].moveDistance > 60 ) log(`${this.segments[0].moveDistance}`); - } - - // Progress through each speed attribute in turn. - const categoryIter = [...SPEED.CATEGORIES].values(); - let speedCategory = categoryIter.next().value; - let maxDistance = SPEED.maximumCategoryDistance(token, speedCategory, tokenSpeed); - - // Determine which speed category we are starting with - // Add in already moved combat distance and determine the starting category - if ( game.combat?.started - && Settings.get(Settings.KEYS.SPEED_HIGHLIGHTING.COMBAT_HISTORY) ) { - - totalCombatMoveDistance = token.lastMoveDistance; - minDistance = totalCombatMoveDistance; - } - - while ( (segment = this.segments[s]) ) { - // Skip speed categories that do not provide a distance larger than the last. - while ( speedCategory && maxDistance <= minDistance ) { - speedCategory = categoryIter.next().value; - maxDistance = SPEED.maximumCategoryDistance(token, speedCategory, tokenSpeed); - } - if ( !speedCategory ) speedCategory = SPEED.CATEGORIES.at(-1); - - segment.speed = speedCategory; - let newPrevDiagonal = _measureSegment(segment, token, numPrevDiagonal); - - // If we have exceeded maxDistance, determine if a split is required. - const newDistance = totalCombatMoveDistance + segment.moveDistance; - - if ( newDistance > maxDistance || newDistance.almostEqual(maxDistance ) ) { - if ( newDistance > maxDistance ) { - // Split the segment, inserting the latter portion in the queue for future iteration. - const splitDistance = maxDistance - totalCombatMoveDistance; - const breakpoint = locateSegmentBreakpoint(segment, splitDistance, token, gridless); - if ( breakpoint ) { - const segments = _splitSegmentAt(segment, breakpoint); - this.segments.splice(s, 1, segments[0]); // Delete the old segment, replace. - this.segments.splice(s + 1, 0, segments[1]); // Add the split. - segment = segments[0]; - newPrevDiagonal = _measureSegment(segment, token, numPrevDiagonal); - } - } - - // Increment to the next speed category. - // Next category will be selected in the while loop above: first category to exceed minDistance. - minDistance = maxDistance; - } - - // Increment totals. - s += 1; - totalDistance += segment.distance; - totalMoveDistance += segment.moveDistance; - totalCombatMoveDistance += segment.moveDistance; - numPrevDiagonal = newPrevDiagonal; - } - - this.totalDistance = totalDistance; - this.totalMoveDistance = totalMoveDistance; -} - -/** - * Determine the specific point at which to cut a ruler segment such that the first subsegment - * measures a specific incremental move distance. - * @param {RulerMeasurementSegment} segment Segment, with ray property, to split - * @param {number} incrementalMoveDistance Distance, in grid units, of the desired first subsegment move distance - * @param {Token} token Token to use when measuring move distance - * @returns {Point3d|null} - * If the incrementalMoveDistance is less than 0, returns null. - * If the incrementalMoveDistance is greater than segment move distance, returns null - * Otherwise returns the point at which to break the segment. - */ -function locateSegmentBreakpoint(segment, splitMoveDistance, token, gridless) { - if ( splitMoveDistance <= 0 ) return null; - if ( !segment.moveDistance || splitMoveDistance > segment.moveDistance ) return null; - - // Attempt to move the split distance and determine the split location. - const { A, B } = segment.ray; - const res = Ruler.measureMoveDistance(A, B, - { token, gridless, useAllElevation: false, stopTarget: splitMoveDistance }); - - let breakpoint = pointFromGridCoordinates(res.endGridCoords); // We can get the exact split point. - if ( !gridless ) { - // We can get the end grid. - // Use halfway between the intersection points for this grid shape. - breakpoint = Point3d.fromObject(segmentGridHalfIntersection(breakpoint, A, B) ?? A); - if ( breakpoint === A ) breakpoint.z = A.z; - else breakpoint.z = canvasElevationFromCoordinates(res.endGridCoords); - } - - if ( breakpoint.almostEqual(B) || breakpoint.almostEqual(A) ) return null; - return breakpoint; -} - -/** - * Cut a ruler segment at a specified point. Does not remeasure the resulting segments. - * Assumes without testing that the breakpoint lies on the segment between A and B. - * @param {RulerMeasurementSegment} segment Segment, with ray property, to split - * @param {Point3d} breakpoint Point to use when splitting the segments - * @returns [RulerMeasurementSegment, RulerMeasurementSegment] - */ -function _splitSegmentAt(segment, breakpoint) { - const { A, B } = segment.ray; - - // Split the segment into two at the break point. - const s0 = {...segment}; - s0.ray = new Ray3d(A, breakpoint); - s0.distance = null; - s0.moveDistance = null; - s0.numDiagonal = null; - - const s1 = {...segment}; - s1.ray = new Ray3d(breakpoint, B); - s1.distance = null; - s1.moveDistance = null; - s1.numPrevDiagonal = null; - s1.numDiagonal = null; - s1.speed = null; - - if ( segment.first ) { s1.first = false; } - if ( segment.last ) { s0.last = false; } - return [s0, s1]; -} - -/** - * For a given segment, locate its intersection at a grid shape. - * The intersection point is on the segment, halfway between the two intersections for the shape. - * @param {number[]} gridCoords - * @param {PIXI.Point} a - * @param {PIXI.Point} b - * @returns {PIXI.Point|undefined} Undefined if no intersection. If only one intersection, the - * endpoint contained within the shape. - */ -function segmentGridHalfIntersection(gridCoords, a, b) { - const shape = gridShape(gridCoords); - const ixs = shape.segmentIntersections(a, b); - if ( !ixs || ixs.length === 0 ) return null; - if ( ixs.length === 1 ) return shape.contains(a.x, a.y) ? a : b; - return PIXI.Point.midPoint(ixs[0], ixs[1]); -} // ----- NOTE: Event handling ----- // @@ -656,7 +478,6 @@ async function teleport(_context) { PATCHES.BASIC.METHODS = { incrementElevation, decrementElevation, - _computeTokenSpeed, teleport }; diff --git a/scripts/segments.js b/scripts/segments.js index 09d62b3..bd22ac1 100644 --- a/scripts/segments.js +++ b/scripts/segments.js @@ -17,6 +17,7 @@ import { perpendicularPoints, log } from "./util.js"; import { Pathfinder, hasCollision } from "./pathfinding/pathfinding.js"; import { userElevationChangeAtWaypoint, elevationFromWaypoint, groundElevationAtWaypoint } from "./terrain_elevation.js"; import { MovePenalty } from "./MovePenalty.js"; +import { tokenSpeedSegmentSplitter } from "./token_speed.js"; /** * Mixed wrap of Ruler.prototype._getMeasurementSegments @@ -263,44 +264,66 @@ export async function _animateSegment(token, segment, destination) { } // ----- NOTE: Segment highlighting ----- // + +const TOKEN_SPEED_SPLITTER = new WeakMap(); + /** * Wrap Ruler.prototype._highlightMeasurementSegment + * @param {RulerMeasurementSegment} segment */ export function _highlightMeasurementSegment(wrapped, segment) { - // Temporarily ensure the ray distance is two-dimensional, so highlighting selects correct squares. + // Temporarily override cached ray.distance such that the ray distance is two-dimensional, + // so highlighting selects correct squares. // Otherwise the highlighting algorithm can get confused for high-elevation segments. segment.ray._distance = PIXI.Point.distanceBetween(segment.ray.A, segment.ray.B); // Adjust the color if this user has selected speed highlighting. - const priorColor = this.color; - const doSpeedHighlighting = segment.speed?.color && Settings.useSpeedHighlighting(this.token); - - // Highlight each split in turn, changing highlight color each time. - if ( doSpeedHighlighting ) this.color = segment.speed.color; + if ( Settings.useSpeedHighlighting(this.token) ) { + if ( segment.first ) TOKEN_SPEED_SPLITTER.set(this.token, tokenSpeedSegmentSplitter(this, this.token)) + const splitterFn = TOKEN_SPEED_SPLITTER.get(this.token); + if ( splitterFn ) { + const priorColor = this.color; + const segments = splitterFn(segment); + if ( segments.length ) { + for ( const segment of segments ) { + this.color = segment.speed.color; + segment.ray._distance = PIXI.Point.distanceBetween(segment.ray.A, segment.ray.B); + wrapped(segment); + + // If gridless, highlight a rectangular shaped portion of the line. + if ( canvas.grid.isGridless ) highlightLineRectangle(segment, this.color, this.name); + } + // Reset to the default color. + this.color = priorColor; + return; + } + } + } - // Call Foundry version and return if not speed highlighting. - const res = wrapped(segment); + wrapped(segment); segment.ray._distance = undefined; // Reset the distance measurement. - if ( !doSpeedHighlighting ) return res; - - // If gridless, highlight a rectangular shaped portion of the line. - if ( canvas.grid.type === CONST.GRID_TYPES.GRIDLESS ) { - const { A, B } = segment.ray; - const width = Math.floor(canvas.scene.dimensions.size * (CONFIG[MODULE_ID].gridlessHighlightWidthMultiplier ?? 0.2)); - const ptsA = perpendicularPoints(A, B, width * 0.5); - const ptsB = perpendicularPoints(B, A, width * 0.5); - const shape = new PIXI.Polygon([ - ptsA[0], - ptsA[1], - ptsB[0], - ptsB[1] - ]); - canvas.interface.grid.highlightPosition(this.name, { x: A.x, y: A.y, color: this.color, shape}); - } +} - // Reset to the default color. - this.color = priorColor; +/** + * Highlight a rectangular shaped portion of the line. + * For use on gridless maps where ruler does not highlight. + * @param {RulerMeasurementSegment} segment + * @param {Color} color Color to use + * @param {string} name Name of the ruler for tracking the highlight graphics + */ +function highlightLineRectangle(segment, color, name) { + const { A, B } = segment.ray; + const width = Math.floor(canvas.scene.dimensions.size * (CONFIG[MODULE_ID].gridlessHighlightWidthMultiplier ?? 0.2)); + const ptsA = perpendicularPoints(A, B, width * 0.5); + const ptsB = perpendicularPoints(B, A, width * 0.5); + const shape = new PIXI.Polygon([ + ptsA[0], + ptsA[1], + ptsB[0], + ptsB[1] + ]); + canvas.interface.grid.highlightPosition(name, { x: A.x, y: A.y, color, shape}); } /** @@ -324,7 +347,6 @@ function elevateSegments(ruler, segments) { // Add destination as the final way const waypoints = [...ruler.waypoints, destWaypoint]; // Add the waypoint elevations to the corresponding segment endpoints. - let currWaypoint; for ( const segment of segments ) { const ray = segment.ray; const startWaypoint = waypoints.find(w => w.x === ray.A.x && w.y === ray.A.y); @@ -378,7 +400,6 @@ function segmentElevationLabel(s) { 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 Bz = s.ray.B.z.toNearest(multiple); const segmentArrow = (increment > 0) ? "↑" : (increment < 0) ? "↓" : "↕"; diff --git a/scripts/token_speed.js b/scripts/token_speed.js new file mode 100644 index 0000000..3598ffd --- /dev/null +++ b/scripts/token_speed.js @@ -0,0 +1,184 @@ +/* globals +canvas, +CONST, +game, +PIXI, +Ruler +*/ +"use strict"; + +import { SPEED } from "./const.js"; +import { Settings } from "./settings.js"; +import { measureSegment } from "./Ruler.js"; +import { Point3d } from "./geometry/3d/Point3d.js"; +import { Ray3d } from "./geometry/3d/Ray3d.js"; +import { gridShape, pointFromGridCoordinates, canvasElevationFromCoordinates } from "./grid_coordinates.js"; + +// Functions used to determine token speed colors. + +/** + * Provides a function to track movement speed for a given group of segments. + * The returned function assumes each segment will be passed in order. + * @param {Ruler} ruler Reference to the ruler + * @param {Token} token Movement token + * @returns {function} Call this function to get the colors for this segment + * - @param {RulerMeasurementSegment} segment The next segment for the token move + * - @returns {RulerMeasurementSegment[]} The split segments, with colors identified for each + */ +export function tokenSpeedSegmentSplitter(ruler, token) { + const defaultColor = ruler.color; + + // Other constants + const gridless = canvas.grid.type === CONST.GRID_TYPES.GRIDLESS; + + // Variables changed in the loop + let totalCombatMoveDistance = 0; + let minDistance = 0; + let numPrevDiagonal = 0; + + // Precalculate the token speed. + const tokenSpeed = SPEED.tokenSpeed(token); + + // Progress through each speed attribute in turn. + const categoryIter = [...SPEED.CATEGORIES].values(); + let speedCategory = categoryIter.next().value; + let maxDistance = SPEED.maximumCategoryDistance(token, speedCategory, tokenSpeed); + + // Determine which speed category we are starting with + // Add in already moved combat distance and determine the starting category + if ( game.combat?.started + && Settings.get(Settings.KEYS.SPEED_HIGHLIGHTING.COMBAT_HISTORY) ) { + + totalCombatMoveDistance = token.lastMoveDistance; + minDistance = totalCombatMoveDistance; + } + + return segment => { + if ( !tokenSpeed ) { + segment.speed = defaultColor; + return [segment]; + } + + const processed = []; + const unprocessed = [segment] + while ( (segment = unprocessed.pop()) ) { + // Skip speed categories that do not provide a distance larger than the last. + while ( speedCategory && maxDistance <= minDistance ) { + speedCategory = categoryIter.next().value; + maxDistance = SPEED.maximumCategoryDistance(token, speedCategory, tokenSpeed); + } + if ( !speedCategory ) speedCategory = SPEED.CATEGORIES.at(-1); + + segment.speed = speedCategory; + let newPrevDiagonal = measureSegment(segment, token, numPrevDiagonal); + + // If we have exceeded maxDistance, determine if a split is required. + const newDistance = totalCombatMoveDistance + segment.moveDistance; + + if ( newDistance > maxDistance || newDistance.almostEqual(maxDistance ) ) { + if ( newDistance > maxDistance ) { + // Split the segment, inserting the latter portion in the queue for future iteration. + const splitDistance = maxDistance - totalCombatMoveDistance; + const breakpoint = locateSegmentBreakpoint(segment, splitDistance, token, gridless); + if ( breakpoint ) { + const segments = _splitSegmentAt(segment, breakpoint); + unprocessed.push(segments[1]); + segment = segments[0]; + newPrevDiagonal = measureSegment(segment, token, numPrevDiagonal); + } + } + + // Increment to the next speed category. + // Next category will be selected in the while loop above: first category to exceed minDistance. + minDistance = maxDistance; + } + + // Increment totals. + totalCombatMoveDistance += segment.moveDistance; + numPrevDiagonal = newPrevDiagonal; + } + return processed; + }; +} + +/** + * Determine the specific point at which to cut a ruler segment such that the first subsegment + * measures a specific incremental move distance. + * @param {RulerMeasurementSegment} segment Segment, with ray property, to split + * @param {number} incrementalMoveDistance Distance, in grid units, of the desired first subsegment move distance + * @param {Token} token Token to use when measuring move distance + * @returns {Point3d|null} + * If the incrementalMoveDistance is less than 0, returns null. + * If the incrementalMoveDistance is greater than segment move distance, returns null + * Otherwise returns the point at which to break the segment. + */ +function locateSegmentBreakpoint(segment, splitMoveDistance, token, gridless) { + if ( splitMoveDistance <= 0 ) return null; + if ( !segment.moveDistance || splitMoveDistance > segment.moveDistance ) return null; + + // Attempt to move the split distance and determine the split location. + const { A, B } = segment.ray; + const res = Ruler.measureMoveDistance(A, B, + { token, gridless, useAllElevation: false, stopTarget: splitMoveDistance }); + + let breakpoint = pointFromGridCoordinates(res.endGridCoords); // We can get the exact split point. + if ( !gridless ) { + // We can get the end grid. + // Use halfway between the intersection points for this grid shape. + breakpoint = Point3d.fromObject(segmentGridHalfIntersection(breakpoint, A, B) ?? A); + if ( breakpoint === A ) breakpoint.z = A.z; + else breakpoint.z = canvasElevationFromCoordinates(res.endGridCoords); + } + + if ( breakpoint.almostEqual(B) || breakpoint.almostEqual(A) ) return null; + return breakpoint; +} + +/** + * Cut a ruler segment at a specified point. Does not remeasure the resulting segments. + * Assumes without testing that the breakpoint lies on the segment between A and B. + * @param {RulerMeasurementSegment} segment Segment, with ray property, to split + * @param {Point3d} breakpoint Point to use when splitting the segments + * @returns [RulerMeasurementSegment, RulerMeasurementSegment] + */ +function _splitSegmentAt(segment, breakpoint) { + const { A, B } = segment.ray; + + // Split the segment into two at the break point. + const s0 = {...segment}; + s0.ray = new Ray3d(A, breakpoint); + s0.distance = null; + s0.moveDistance = null; + s0.numDiagonal = null; + + const s1 = {...segment}; + s1.ray = new Ray3d(breakpoint, B); + s1.distance = null; + s1.moveDistance = null; + s1.numPrevDiagonal = null; + s1.numDiagonal = null; + s1.speed = null; + + if ( segment.first ) { s1.first = false; } + if ( segment.last ) { s0.last = false; } + return [s0, s1]; +} + +/** + * For a given segment, locate its intersection at a grid shape. + * The intersection point is on the segment, halfway between the two intersections for the shape. + * @param {number[]} gridCoords + * @param {PIXI.Point} a + * @param {PIXI.Point} b + * @returns {PIXI.Point|undefined} Undefined if no intersection. If only one intersection, the + * endpoint contained within the shape. + */ +function segmentGridHalfIntersection(gridCoords, a, b) { + const shape = gridShape(gridCoords); + const ixs = shape.segmentIntersections(a, b); + if ( !ixs || ixs.length === 0 ) return null; + if ( ixs.length === 1 ) return shape.contains(a.x, a.y) ? a : b; + return PIXI.Point.midPoint(ixs[0], ixs[1]); +} + + From 78e81c40cbd5c04de80df3586f4a9bf5f6689e19 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Wed, 26 Jun 2024 18:19:09 -0700 Subject: [PATCH 02/23] =?UTF-8?q?=F0=9F=90=9B=20fix|MovementType|Use=20dnd?= =?UTF-8?q?5e=20status=20for=20movement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the token hud for dnd5e. Rely on statuses instead. Add setting for dnd5e to autodetect. Fix autodetect to use the Ruler's elevation at point method. --- languages/en.json | 3 +++ scripts/patching.js | 3 ++- scripts/settings.js | 17 ++++++++++++++++- scripts/token_hud.js | 30 +++++++++++++++++------------- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/languages/en.json b/languages/en.json index 5714c6c..eddbd16 100644 --- a/languages/en.json +++ b/languages/en.json @@ -91,6 +91,9 @@ "elevationruler.settings.grid-terrain-area-threshold.name": "Percent Area Threshold", "elevationruler.settings.grid-terrain-area-threshold.hint": "If 'Percent Area' is selected for Terrain Grid Measurement, this determines the percent overlap required for the terrain/token to count when measuring on grids.", + "elevationruler.settings.automatic-movement-type.name": "Autodetect Movement Type", + "elevationruler.settings.automatic-movement-type.hint": "Automatically detect the movement type based on token position. Set the token status effect to 'fly' or 'burrowing' to override.", + "elevationruler.controls.prefer-token-elevation.name": "Prefer Token Elevation", "elevationruler.controls.pathfinding-control.name": "Use Pathfinding", diff --git a/scripts/patching.js b/scripts/patching.js index 4147023..89868f4 100644 --- a/scripts/patching.js +++ b/scripts/patching.js @@ -42,6 +42,7 @@ export function initializePatching() { PATCHER.registerGroup("TOKEN_RULER"); PATCHER.registerGroup("SPEED_HIGHLIGHTING"); PATCHER.registerGroup("MOVEMENT_TRACKING"); - PATCHER.registerGroup("MOVEMENT_SELECTION"); + + if ( game.system.id !== "dnd5e" ) PATCHER.registerGroup("MOVEMENT_SELECTION"); } diff --git a/scripts/settings.js b/scripts/settings.js index 8c1e2cc..41e1819 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -65,7 +65,9 @@ const SETTINGS = { EUCLIDEAN: "grid-terrain-choice-euclidean" }, AREA_THRESHOLD: "grid-terrain-area-threshold" - } + }, + + AUTO_MOVEMENT_TYPE: "automatic-movement-type" }; const KEYBINDINGS = { @@ -213,6 +215,19 @@ export class Settings extends ModuleSettingsAbstract { requiresReload: false }); + if ( game.system.id === "dnd5e" ) { + register(KEYS.AUTO_MOVEMENT_TYPE, { + name: localize(`${KEYS.AUTO_MOVEMENT_TYPE}.name`), + hint: localize(`${KEYS.AUTO_MOVEMENT_TYPE}.hint`), + scope: "user", + config: true, + default: true, + 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/token_hud.js b/scripts/token_hud.js index 8111ade..ca00d64 100644 --- a/scripts/token_hud.js +++ b/scripts/token_hud.js @@ -1,5 +1,6 @@ /* globals -canvas +game, +Ruler */ "use strict"; /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ @@ -29,11 +30,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { MODULE_ID, FLAGS, MOVEMENT_TYPES, MOVEMENT_BUTTONS, MODULES_ACTIVE } from "./const.js"; -import { LevelsElevationAtPoint } from "./terrain_elevation.js"; +import { MODULE_ID, FLAGS, MOVEMENT_TYPES, MOVEMENT_BUTTONS } from "./const.js"; +import { Settings } from "./settings.js"; import { keyForValue } from "./util.js"; export const PATCHES = {}; +PATCHES.BASIC = {}; PATCHES.MOVEMENT_SELECTION = {}; /** @@ -55,26 +57,28 @@ PATCHES.MOVEMENT_SELECTION.HOOKS = { renderTokenHUD }; * @type {MOVEMENT_TYPE} */ function movementType() { - let selectedMovement = this.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_SELECTION) ?? MOVEMENT_TYPES.AUTO; + // For dnd5e, use status types + if ( game.system.id === "dnd5e" ) { + if ( this.actor.statuses.has("flying") ) return "FLY"; + if ( this.actor.statuses.has("burrow") ) return "BURROW"; + if ( Settings.get(Settings.KEYS.AUTO_MOVEMENT_TYPE) ) return determineMovementType(this); + return "WALK"; + } + + // Otherwise, use the Token HUD. + const selectedMovement = this.document.getFlag(MODULE_ID, FLAGS.MOVEMENT_SELECTION) ?? MOVEMENT_TYPES.AUTO; if ( selectedMovement === MOVEMENT_TYPES.AUTO ) return determineMovementType(this); return keyForValue(MOVEMENT_TYPES, selectedMovement); } -PATCHES.MOVEMENT_SELECTION.GETTERS = { movementType }; +PATCHES.BASIC.GETTERS = { movementType }; /** * Determine movement type based on this token's elevation. * @returns {MOVEMENT_TYPE} */ function determineMovementType(token) { - let groundElevation; - if ( MODULES_ACTIVE.ELEVATED_VISION ) { - const calc = new canvas.elevation.TokenElevationCalculator(token); - groundElevation = calc.groundElevation(); - } else if ( MODULES_ACTIVE.LEVELS ) { - groundElevation = LevelsElevationAtPoint(token.center, { startingElevation: token.elevationE }) ?? 0; - } else groundElevation = 0; - + const groundElevation = Ruler.terrainElevationAtLocation(token.center); return keyForValue(MOVEMENT_TYPES, Math.sign(token.elevationE - groundElevation) + 1); } From 688d7d59f95ebd1fbcf5b15ec2a42d3bef46418f Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Wed, 26 Jun 2024 20:24:04 -0700 Subject: [PATCH 03/23] =?UTF-8?q?=F0=9F=90=9B=20fix|TokenSpeed|Fix=20displ?= =?UTF-8?q?ay=20of=20token=20speed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/token_speed.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/token_speed.js b/scripts/token_speed.js index 3598ffd..7dc190b 100644 --- a/scripts/token_speed.js +++ b/scripts/token_speed.js @@ -94,6 +94,7 @@ export function tokenSpeedSegmentSplitter(ruler, token) { } // Increment totals. + processed.push(segment); totalCombatMoveDistance += segment.moveDistance; numPrevDiagonal = newPrevDiagonal; } From 020f7acedad487bad29f0f0b21e9ff0e7ee70bcb Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Wed, 26 Jun 2024 20:25:05 -0700 Subject: [PATCH 04/23] =?UTF-8?q?=F0=9F=92=A1=20refactor|Terrain|Drop=20te?= =?UTF-8?q?rrain=20choice=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently not implemented for Terrain Mapper --- scripts/MovePenalty.js | 4 +-- scripts/settings.js | 72 +++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/scripts/MovePenalty.js b/scripts/MovePenalty.js index c148af0..9cf8432 100644 --- a/scripts/MovePenalty.js +++ b/scripts/MovePenalty.js @@ -466,10 +466,10 @@ export class TerrainMovePenaltyGridless extends MovePenaltyGridless { export class MovePenaltyGridded extends MovePenalty { /** @type {Settings.KEYS.GRID_TERRAIN.CHOICES} */ - static get griddedAlgorithm() { return Settings.get(Settings.KEYS.GRID_TERRAIN.ALGORITHM); } + static get griddedAlgorithm() { return "grid-terrain-choice-center-point"; } // return Settings.get(Settings.KEYS.GRID_TERRAIN.ALGORITHM); } /** @type {number} */ - static get percentAreaThreshold() { return Settings.get(Settings.KEYS.GRID_TERRAIN.AREA_THRESHOLD); } + static get percentAreaThreshold() { return 0.5; } // return Settings.get(Settings.KEYS.GRID_TERRAIN.AREA_THRESHOLD); } /** * Returns a penalty function for gridded moves. diff --git a/scripts/settings.js b/scripts/settings.js index 41e1819..aa464fe 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -57,15 +57,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" +// }, AUTO_MOVEMENT_TYPE: "automatic-movement-type" }; @@ -252,33 +252,33 @@ export class Settings extends ModuleSettingsAbstract { }); // ----- 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() { From 80f07d87eecc54ae1582810692a55efb94ee105d Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Wed, 26 Jun 2024 20:31:11 -0700 Subject: [PATCH 05/23] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20docs|Changelog|Updat?= =?UTF-8?q?e=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Changelog.md b/Changelog.md index 7d61ca6..bff4f4b 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,8 @@ +# 0.9.6 +Move calculation of speed colors to `Ruler#_highlightMeasurementSegment`. As a result, Ruler segments are not split at movement speed category changes, so there are no longer extra waypoints added. +Remove settings related to determining when a terrain affects a token. Currently, Foundry regions only checks the center point. +For dnd5e, remove the Token HUD control to select movement, because dnd5e now uses status effects to signify when tokens are flying or burrowing. Added a setting to automatically determine token movement for dnd5e. + # 0.9.5 Added Brazilian translation. Thanks @Kharmans! Added Russian translation. Thanks @VirusNik21! Closes #96. From c2daa7de4f8d50819fce8032ffb1ec5da0aa4f0f Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sat, 29 Jun 2024 17:36:05 -0700 Subject: [PATCH 06/23] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20docs|Translations|Up?= =?UTF-8?q?dated=20Polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From Lioheart. Issue #105. --- languages/pl.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/languages/pl.json b/languages/pl.json index 7275b37..986eb7a 100644 --- a/languages/pl.json +++ b/languages/pl.json @@ -6,11 +6,17 @@ "elevationruler.keybindings.incrementElevation.name": "Zwiększenie wysokości linijki", "elevationruler.keybindings.incrementElevation.hint": "Zwiększa wysokość w odstępach siatki podczas korzystania z linijki.", + "elevationruler.keybindings.addWaypoint.name": "Dodanie punktu trasy zwykłej linijki", + "elevationruler.keybindings.addWaypoint.hint": "Podczas korzystania z linijki na płótnie dodaj punkt trasy.", + + "elevationruler.keybindings.removeWaypoint.name": "Usunięcie punktu trasy zwykłej linijki", + "elevationruler.keybindings.removeWaypoint.hint": "Podczas korzystania z linijki na płótnie usuń punkt trasy.", + "elevationruler.keybindings.addWaypointTokenRuler.name": "Dodanie punktu trasy dla tokena", - "elevationruler.keybindings.addWaypointTokenRuler.hint": "Gdy linijka tokena jest włączona, dodaje punkt trasy podczas przeciągania tokena.", + "elevationruler.keybindings.addWaypointTokenRuler.hint": "Gdy linijka tokenów jest włączona i przeciągasz token, dodaj punkt trasy.", "elevationruler.keybindings.removeWaypointTokenRuler.name": "Usunięcie punktu trasy tokena", - "elevationruler.keybindings.removeWaypointTokenRuler.hint": "Gdy linijka tokena jest włączona, usuwa punkt trasy podczas przeciągania tokena.", + "elevationruler.keybindings.removeWaypointTokenRuler.hint": "Gdy linijka tokenów jest włączona i przeciągasz token, usuń punkt trasy.", "elevationruler.keybindings.togglePathfinding.name": "Tymczasowe wyłączenie wyszukiwania ścieżek", "elevationruler.keybindings.togglePathfinding.hint": "Jeśli przycisk wyszukiwania ścieżki jest włączony, przytrzymanie tego przycisku spowoduje tymczasowe wyłączenie wyszukiwania ścieżki. Jeśli przycisk odnajdywania ścieżek nie jest włączony, przytrzymanie tego klawisza tymczasowo włączy odnajdywanie ścieżek.", @@ -33,6 +39,9 @@ "elevationruler.settings.enable-token-ruler.name": "Używanie linijki tokenów", "elevationruler.settings.enable-token-ruler.hint": "Wyświetlanie linijki podczas przeciągania tokenów.", + "elevationruler.settings.hide-gm-ruler.name": "Ukryj linijkę MG", + "elevationruler.settings.hide-gm-ruler.hint": "Nie wyświetlaj linijki tokena MG innym użytkownikom niebędącym MG.", + "elevationruler.settings.speed-highlighting-choice.name": "Używanie podświetlenia prędkości tokenów", "elevationruler.settings.speed-highlighting-choice.hint": "Podczas korzystania z linijki, używaj różnych kolorów dla tokenów chodu/dystansu/maksymalnego dystansu.", From c25c61f4593235889ab01690acf80ed192b89066 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 07:40:21 -0700 Subject: [PATCH 07/23] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20docs|Changelog|Add?= =?UTF-8?q?=20to=20log=20re=20polish=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index bff4f4b..5b7389f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,8 @@ Move calculation of speed colors to `Ruler#_highlightMeasurementSegment`. As a result, Ruler segments are not split at movement speed category changes, so there are no longer extra waypoints added. Remove settings related to determining when a terrain affects a token. Currently, Foundry regions only checks the center point. For dnd5e, remove the Token HUD control to select movement, because dnd5e now uses status effects to signify when tokens are flying or burrowing. Added a setting to automatically determine token movement for dnd5e. +Updated Polish translation. Thanks @Lioheart! Closes #105. + # 0.9.5 Added Brazilian translation. Thanks @Kharmans! From edb6c9bc7978314767cc651207fd3903bb339378 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 07:46:34 -0700 Subject: [PATCH 08/23] =?UTF-8?q?=F0=9F=90=9B=20fix|Speed|Catch=20if=20`ac?= =?UTF-8?q?tor.statuses`=20is=20undefined?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Likely from token with actor no longer in scene. --- scripts/token_hud.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/token_hud.js b/scripts/token_hud.js index ca00d64..4712c39 100644 --- a/scripts/token_hud.js +++ b/scripts/token_hud.js @@ -59,6 +59,7 @@ PATCHES.MOVEMENT_SELECTION.HOOKS = { renderTokenHUD }; function movementType() { // For dnd5e, use status types if ( game.system.id === "dnd5e" ) { + if ( !this.actor?.statuses ) return "WALK"; if ( this.actor.statuses.has("flying") ) return "FLY"; if ( this.actor.statuses.has("burrow") ) return "BURROW"; if ( Settings.get(Settings.KEYS.AUTO_MOVEMENT_TYPE) ) return determineMovementType(this); From e18a2daf477d134b98de80473a1fd53d8e2bcefa Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 07:59:13 -0700 Subject: [PATCH 09/23] =?UTF-8?q?=F0=9F=90=9B=20fix|Flags|Add=20SCENE.BACK?= =?UTF-8?q?GROUNDS=20flag=20from=20TM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/const.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/const.js b/scripts/const.js index 9529657..f48588f 100644 --- a/scripts/const.js +++ b/scripts/const.js @@ -15,7 +15,10 @@ export const TEMPLATES = { export const FLAGS = { MOVEMENT_SELECTION: "selectedMovementType", - MOVEMENT_PENALTY: "movementPenalty" + MOVEMENT_PENALTY: "movementPenalty", + SCENE: { + BACKGROUND_ELEVATION: "backgroundElevation" + } }; export const MODULES_ACTIVE = { API: {} }; From fc37f843abac27f89c7745dcfc0d6856a60a1c00 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 08:03:14 -0700 Subject: [PATCH 10/23] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20docs|Changelog|Add?= =?UTF-8?q?=20#107=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index 5b7389f..b5b5db7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -3,6 +3,7 @@ Move calculation of speed colors to `Ruler#_highlightMeasurementSegment`. As a r Remove settings related to determining when a terrain affects a token. Currently, Foundry regions only checks the center point. For dnd5e, remove the Token HUD control to select movement, because dnd5e now uses status effects to signify when tokens are flying or burrowing. Added a setting to automatically determine token movement for dnd5e. Updated Polish translation. Thanks @Lioheart! Closes #105. +Added `FLAGS.SCENE.BACKGROUND`. Closes #107. # 0.9.5 From c202241597acad6f7f807ce0cd0bd31264f785a1 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 08:06:53 -0700 Subject: [PATCH 11/23] =?UTF-8?q?=F0=9F=92=A1=20refactor|Pathfinding|Drop?= =?UTF-8?q?=20unused=20updateToken=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dealt with in token refresh instead. --- scripts/pathfinding/Token.js | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/scripts/pathfinding/Token.js b/scripts/pathfinding/Token.js index bbc24fb..aef4a4a 100644 --- a/scripts/pathfinding/Token.js +++ b/scripts/pathfinding/Token.js @@ -24,37 +24,6 @@ function createToken(document, _options, _userId) { Pathfinder.dirty = true; } -/** - * Hook updateToken to update the scene graph and triangulation. - * @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) { - // Only update the edges if the coordinates have changed. - if ( !(Object.hasOwn(changes, "x") || Object.hasOwn(changes, "y")) ) return; - - log(`updateToken hook|token moved.`); - -// // Easiest approach is to trash the edges for the wall and re-create them. -// SCENE_GRAPH.removeToken(document.id); -// -// /* Debugging: None of the edges should have this token. -// if ( CONFIG[MODULE_ID].debug ) { -// const token = document.object; -// SCENE_GRAPH.edges.forEach((edge, key) => { -// if ( edge.objects.has(token) ) console.debug(`Edge ${key} has ${token.name} ${token.id} after deletion.`); -// }) -// } -// */ -// -// SCENE_GRAPH.addToken(document.object); -// -// // Need to re-do the triangulation because the change to the wall could have added edges if intersected. -// Pathfinder.dirty = true; -} - /** * Hook refresh token to update the scene graph and triangulation. * Cannot use updateToken hook b/c the token position is not correctly updated by that point. @@ -94,4 +63,4 @@ function deleteToken(document, _options, _userId) { Pathfinder.dirty = true; } -PATCHES.PATHFINDING_TOKENS.HOOKS = { createToken, updateToken, deleteToken, refreshToken }; +PATCHES.PATHFINDING_TOKENS.HOOKS = { createToken, deleteToken, refreshToken }; From 02f2b6f0252797baa515f08c8beccc79420e553c Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 08:41:57 -0700 Subject: [PATCH 12/23] =?UTF-8?q?=F0=9F=8E=B8=20feat|Pathfinding|Add=20`to?= =?UTF-8?q?kenPathfindingBuffer`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modifies the token shape to allow pathfinding around the edge of the token. --- scripts/module.js | 8 ++++++++ scripts/pathfinding/WallTracer.js | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/module.js b/scripts/module.js index b2af8b9..5ce54cc 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -109,6 +109,14 @@ Hooks.once("init", function() { */ gridlessHighlightWidthMultiplier: 0.2, + /** + * Amount, in pixels, to pad the token shape that is used when pathfinding around tokens. + * Negative amounts allow the pathfinding to move through outer border of the token. + * Positive amounts make tokens larger than they appear, creating a buffer. + * @type {number} + */ + tokenPathfindingBuffer: -1, + /** * Enable certain debug console logging and tests. * @type {boolean} diff --git a/scripts/pathfinding/WallTracer.js b/scripts/pathfinding/WallTracer.js index 755818d..0217a10 100644 --- a/scripts/pathfinding/WallTracer.js +++ b/scripts/pathfinding/WallTracer.js @@ -658,8 +658,13 @@ export class WallTracer extends Graph { const tokenId = token.id; if ( this.edges.has(tokenId) ) return; + // Pad the constrained token border as necessary. + const borderShape = token.constrainedTokenBorder; + const buffer = CONFIG[MODULE_ID].tokenPathfindingBuffer ?? 0; + if ( buffer ) borderShape.pad(buffer); + // Construct a new token edge set. - const edgeIter = token.constrainedTokenBorder.iterateEdges(); + const edgeIter = borderShape.iterateEdges(); for ( const edge of edgeIter ) this.addObjectEdge(edge.A, edge.B, token); this.tokenIds.add(tokenId); } From 54fca70f9001a25073b8ee45ff64d569926ad2e1 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 08:42:33 -0700 Subject: [PATCH 13/23] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20docs|Changelog|Add?= =?UTF-8?q?=20issue=20107?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changelog.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index b5b5db7..21a1373 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,10 +1,10 @@ # 0.9.6 +Added `CONFIG.elevationruler.tokenPathfindingBuffer`. This defaults to -1, allowing movement diagonally when at the corner of a large token and "Tokens Block" is set. For pathfinding, more negative numbers shrink the token border; positive numbers increase it. Note that if you change this, you will need to reload the scene or at least move the tokens before their pathfinding borders will be changed. Move calculation of speed colors to `Ruler#_highlightMeasurementSegment`. As a result, Ruler segments are not split at movement speed category changes, so there are no longer extra waypoints added. Remove settings related to determining when a terrain affects a token. Currently, Foundry regions only checks the center point. For dnd5e, remove the Token HUD control to select movement, because dnd5e now uses status effects to signify when tokens are flying or burrowing. Added a setting to automatically determine token movement for dnd5e. Updated Polish translation. Thanks @Lioheart! Closes #105. -Added `FLAGS.SCENE.BACKGROUND`. Closes #107. - +Added `SCENE.BACKGROUND` to flags (imported from Terrain Mapper). Closes #107. # 0.9.5 Added Brazilian translation. Thanks @Kharmans! From 320b66b6e2794abe1272f723df66cfc39969f5d8 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 08:43:06 -0700 Subject: [PATCH 14/23] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20docs|Changelog|Add?= =?UTF-8?q?=20issue=20#88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 21a1373..0ac31d5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,5 @@ # 0.9.6 -Added `CONFIG.elevationruler.tokenPathfindingBuffer`. This defaults to -1, allowing movement diagonally when at the corner of a large token and "Tokens Block" is set. For pathfinding, more negative numbers shrink the token border; positive numbers increase it. Note that if you change this, you will need to reload the scene or at least move the tokens before their pathfinding borders will be changed. +Added `CONFIG.elevationruler.tokenPathfindingBuffer`. This defaults to -1, allowing movement diagonally when at the corner of a large token and "Tokens Block" is set. For pathfinding, more negative numbers shrink the token border; positive numbers increase it. Note that if you change this, you will need to reload the scene or at least move the tokens before their pathfinding borders will be changed. Closes #88. Move calculation of speed colors to `Ruler#_highlightMeasurementSegment`. As a result, Ruler segments are not split at movement speed category changes, so there are no longer extra waypoints added. Remove settings related to determining when a terrain affects a token. Currently, Foundry regions only checks the center point. For dnd5e, remove the Token HUD control to select movement, because dnd5e now uses status effects to signify when tokens are flying or burrowing. Added a setting to automatically determine token movement for dnd5e. From 4224eeb23f90683e28913751151af9e1313f6df4 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 08:44:50 -0700 Subject: [PATCH 15/23] =?UTF-8?q?=F0=9F=8E=B8=20feat|TokenRuler|Don't=20br?= =?UTF-8?q?oadcast=20token=20ruler=20if=20token=20hidden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Token Ruler will not broadcast if token is hidden, invisible, or secret. Issue #112. --- scripts/Ruler.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 98a98a9..352583f 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -367,6 +367,20 @@ export function measureSegment(segment, token, numPrevDiagonal = 0) { +/** + * Mixed wrap Ruler#_broadcastMeasurement + * For token ruler, don't broadcast the ruler if the token is invisible or disposition secret. + */ +function _broadcastMeasurement(wrapped) { + // Don't broadcast invisible, hidden, or secret token movement when dragging. + if ( this._isTokenRuler + && (this.token.document.disposition === CONST.TOKEN_DISPOSITIONS.SECRET + || this.token.document.hasStatusEffect(CONFIG.specialStatusEffects.INVISIBLE) + || this.token.document.isHidden) ) return; + + wrapped(); +} + // ----- NOTE: Event handling ----- // /** @@ -410,7 +424,7 @@ PATCHES.BASIC.WRAPS = { _onMoveKeyDown }; -PATCHES.BASIC.MIXES = { _animateMovement, _getMeasurementSegments }; +PATCHES.BASIC.MIXES = { _animateMovement, _getMeasurementSegments, _broadcastMeasurement }; PATCHES.BASIC.OVERRIDES = { _computeDistance, _animateSegment, _addWaypoint }; From 7c76ac348ae942b556475ac9e65b91a951363e99 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 08:45:45 -0700 Subject: [PATCH 16/23] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20docs|Changelog|Add?= =?UTF-8?q?=20issue=20#112.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog.md b/Changelog.md index 0ac31d5..3487e2d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -5,6 +5,7 @@ Remove settings related to determining when a terrain affects a token. Currently For dnd5e, remove the Token HUD control to select movement, because dnd5e now uses status effects to signify when tokens are flying or burrowing. Added a setting to automatically determine token movement for dnd5e. Updated Polish translation. Thanks @Lioheart! Closes #105. Added `SCENE.BACKGROUND` to flags (imported from Terrain Mapper). Closes #107. +Don't broadcast the Token Ruler if the token being dragged is secret, invisible, or hidden. Closes #112. # 0.9.5 Added Brazilian translation. Thanks @Kharmans! From b7a007b1fe4969984716e39a20f93c0573bc8a64 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 10:20:45 -0700 Subject: [PATCH 17/23] =?UTF-8?q?=F0=9F=90=9B=20fix|Ruler|Catch=20errors?= =?UTF-8?q?=20if=20token=20is=20not=20defined.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/Ruler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/Ruler.js b/scripts/Ruler.js index 352583f..9777246 100644 --- a/scripts/Ruler.js +++ b/scripts/Ruler.js @@ -155,7 +155,7 @@ function _addWaypoint(point, {snap=true}={}) { // Determine the elevation up until this point if ( !this.waypoints.length ) { waypoint._prevElevation = this.token?.elevationE ?? canvas.scene.getFlag("terrainmapper", FLAGS.SCENE.BACKGROUND_ELEVATION) ?? 0; - waypoint._forceToGround ||= this.token.movementType === "WALK" + waypoint._forceToGround ||= this.token ? this.token.movementType === "WALK" : false; } else waypoint._prevElevation = elevationFromWaypoint(this.waypoints.at(-1), waypoint, this.token); this.waypoints.push(waypoint); @@ -373,6 +373,7 @@ 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.document.disposition === CONST.TOKEN_DISPOSITIONS.SECRET || this.token.document.hasStatusEffect(CONFIG.specialStatusEffects.INVISIBLE) From 51ff76d8f1ee2aa16912d18d19884d264d753a6b Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 11:22:59 -0700 Subject: [PATCH 18/23] =?UTF-8?q?=F0=9F=92=A1=20refactor|Speed|Move=20the?= =?UTF-8?q?=20speed=20defaults=20to=20separate=20file.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/const.js | 171 +----------------------------- scripts/module.js | 3 +- scripts/system_attributes.js | 198 +++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+), 169 deletions(-) create mode 100644 scripts/system_attributes.js diff --git a/scripts/const.js b/scripts/const.js index f48588f..29f2db1 100644 --- a/scripts/const.js +++ b/scripts/const.js @@ -1,5 +1,4 @@ /* globals -Color, foundry, game, Hooks @@ -49,62 +48,9 @@ export const MOVEMENT_BUTTONS = { }; /** - * Below Taken from Drag Ruler + * Properties related to token speed measurement + * See system_attributes.js for Speed definitions for different systems. */ -/* -MIT License - -Copyright (c) 2021 Manuel Vögele - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ - -/** - * @typedef {object} SpeedCategory - * - * Object that stores the name, multiplier, and color of a given speed category. - * Custom properties are permitted. The SpeedCategory is passed to SPEED.maximumCategoryDistance, - * which in turn can be defined to use custom properties to calculate the maximum distance for the category. - * - * @prop {Color} color Color used with ruler highlighting - * @prop {string} name Unique name of the category (relative to other SpeedCategories) - * @prop {number} [multiplier] This times the token movement equals the distance for this category - */ - -const WalkSpeedCategory = { - name: "Walk", - color: Color.from(0x00ff00), - multiplier: 1 -}; - -const DashSpeedCategory = { - name: "Dash", - color: Color.from(0xffff00), - multiplier: 2 -}; - -const MaximumSpeedCategory = { - name: "Maximum", - color: Color.from(0xff0000), - multiplier: Number.POSITIVE_INFINITY -} - export const SPEED = { /** * Object of strings indicating where on the actor to locate the given attribute. @@ -118,7 +64,7 @@ export const SPEED = { * in the first category is the next category considered. * @type {SpeedCategory[]} */ - CATEGORIES: [WalkSpeedCategory, DashSpeedCategory, MaximumSpeedCategory], + CATEGORIES: [], // Use Font Awesome font unicode instead of basic unicode for displaying terrain symbol. @@ -136,7 +82,6 @@ export const SPEED = { terrainSymbol: "🥾" }; - /** * Given a token, get the maximum distance the token can travel for a given type. * Distance measured from 0, so types overlap. E.g. @@ -165,116 +110,6 @@ SPEED.tokenSpeed = function(token) { return Number(speed); }; -// Avoid testing for the system id each time. -Hooks.once("init", function() { - SPEED.ATTRIBUTES.WALK = defaultWalkAttribute(); - SPEED.ATTRIBUTES.BURROW = defaultBurrowAttribute(); - SPEED.ATTRIBUTES.FLY = defaultFlyAttribute(); - DashSpeedCategory.multiplier = defaultDashMultiplier(); -}); - -export function defaultHPAttribute() { - switch ( game.system.id ) { - case "dnd5e": return "actor.system.attributes.hp.value"; - case "dragonbane": return "actor.system.hitpoints.value"; - case "twodsix": return "actor.system.hits.value"; - default: return "actor.system.attributes.hp.value"; - } -} - -export function defaultWalkAttribute() { - switch ( game.system.id ) { - case "CoC7": return "actor.system.attribs.mov.value"; - case "dcc": return "actor.system.attributes.speed.value"; - case "sfrpg": return "actor.system.attributes.speed.value"; - case "dnd4e": return "actor.system.movement.walk.value"; - case "dnd5e": return "actor.system.attributes.movement.walk"; - case "lancer": return "actor.system.derived.speed"; - - case "pf1": - case "D35E": return "actor.system.attributes.speed.land.total"; - case "shadowrun5e": return "actor.system.movement.walk.value"; - case "swade": return "actor.system.stats.speed.adjusted"; - case "ds4": return "actor.system.combatValues.movement.total"; - case "splittermond": return "actor.derivedValues.speed.value"; - case "wfrp4e": return "actor.system.details.move.walk"; - case "crucible": return "actor.system.movement.stride"; - case "dragonbane": return "actor.system.movement"; - case "twodsix": return "actor.system.movement.walk"; - default: return ""; - } -} - -export function defaultFlyAttribute() { - switch ( game.system.id ) { - // Missing attribute case "CoC7": - // Missing attribute case "dcc": - case "sfrpg": return "actor.system.attributes.flying.value"; - // Missing attribute case "dnd4e": - case "dnd5e": return "actor.system.attributes.movement.fly"; - // Missing attribute case "lancer": - case "pf1": - case "D35E": return "actor.system.attributes.speed.fly.total"; - // Missing attribute case "shadowrun5e": - // Missing attribute case "swade": - // Missing attribute case "ds4": - // Missing attribute case "splittermond": - // Missing attribute case "wfrp4e": - // Missing attribute case "crucible": - // Missing attribute case "dragonbane": - case "twodsix": return "actor.system.movement.fly"; - default: return ""; - } -} - -export function defaultBurrowAttribute() { - switch ( game.system.id ) { - // Missing attribute case "CoC7": - // Missing attribute case "dcc": - case "sfrpg": return "actor.system.attributes.burrowing.value"; - // Missing attribute case "dnd4e": - case "dnd5e": return "actor.system.attributes.movement.burrow"; - // Missing attribute case "lancer": - case "pf1": - case "D35E": return "actor.system.attributes.speed.burrow.total"; - // Missing attribute case "shadowrun5e": - // Missing attribute case "swade": - // Missing attribute case "ds4": - // Missing attribute case "splittermond": - // Missing attribute case "wfrp4e": - // Missing attribute case "crucible": - // Missing attribute case "dragonbane": - case "twodsix": return "actor.system.movement.burrow"; - default: return ""; - } -} - -export function defaultDashMultiplier() { - switch ( game.system.id ) { - case "dcc": - case "dnd4e": - case "dnd5e": - case "lancer": - case "pf1": - case "D35E": - case "sfrpg": - case "shadowrun5e": - case "dragonbane": - case "twodsix": - case "ds4": return 2; - - case "CoC7": return 5; - case "splittermond": return 3; - case "wfrp4e": return 2; - - case "crucible": - case "swade": return 0; - default: return 0; - } -} - -/* eslint-enable no-multi-spaces */ - /** * From Foundry v12 * The different rules to define and measure diagonal distance/cost in a square grid. diff --git a/scripts/module.js b/scripts/module.js index 5ce54cc..1d8d78c 100644 --- a/scripts/module.js +++ b/scripts/module.js @@ -10,7 +10,8 @@ ui import { Settings } from "./settings.js"; import { initializePatching, PATCHER } from "./patching.js"; -import { MODULE_ID, MOVEMENT_TYPES, SPEED, MOVEMENT_BUTTONS, defaultHPAttribute } from "./const.js"; +import { MODULE_ID, MOVEMENT_TYPES, MOVEMENT_BUTTONS, SPEED } from "./const.js"; +import { defaultHPAttribute } from "./system_attributes.js"; import { registerGeometry } from "./geometry/registration.js"; // Grid coordinates diff --git a/scripts/system_attributes.js b/scripts/system_attributes.js new file mode 100644 index 0000000..7a54c24 --- /dev/null +++ b/scripts/system_attributes.js @@ -0,0 +1,198 @@ +/* globals +Color, +game, +Hooks +*/ +"use strict"; + +import { SPEED } from "./const.js"; + +/** + * @typedef {object} SpeedCategory + * + * Object that stores the name, multiplier, and color of a given speed category. + * Custom properties are permitted. The SpeedCategory is passed to SPEED.maximumCategoryDistance, + * which in turn can be defined to use custom properties to calculate the maximum distance for the category. + * + * @prop {Color} color Color used with ruler highlighting + * @prop {string} name Unique name of the category (relative to other SpeedCategories) + * @prop {number} [multiplier] This times the token movement equals the distance for this category + */ + +const WalkSpeedCategory = { + name: "Walk", + color: Color.from(0x00ff00), + multiplier: 1 +}; + +const DashSpeedCategory = { + name: "Dash", + color: Color.from(0xffff00), + multiplier: 2 +}; + +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. + SPEED.ATTRIBUTES.WALK = defaultWalkAttribute(); + SPEED.ATTRIBUTES.BURROW = defaultBurrowAttribute(); + SPEED.ATTRIBUTES.FLY = defaultFlyAttribute(); + DashSpeedCategory.multiplier = defaultDashMultiplier(); + SPEED.CATEGORIES = [WalkSpeedCategory, DashSpeedCategory, MaximumSpeedCategory]; +}); + +// ----- NOTE: Attributes ----- // + +/** + * Some of below taken from Drag Ruler + */ +/* +MIT License + +Copyright (c) 2021 Manuel Vögele + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Location of the HP attribute for a given system's actor. + * @returns {string} + */ +export function defaultHPAttribute() { + switch ( game.system.id ) { + case "dnd5e": return "actor.system.attributes.hp.value"; + case "dragonbane": return "actor.system.hitpoints.value"; + case "twodsix": return "actor.system.hits.value"; + default: return "actor.system.attributes.hp.value"; + } +} + +/** + * Location of the walk attribute for a given system's actor. + * @returns {string} + */ +export function defaultWalkAttribute() { + switch ( game.system.id ) { + case "CoC7": return "actor.system.attribs.mov.value"; + case "dcc": return "actor.system.attributes.speed.value"; + case "sfrpg": return "actor.system.attributes.speed.value"; + case "dnd4e": return "actor.system.movement.walk.value"; + case "dnd5e": return "actor.system.attributes.movement.walk"; + case "lancer": return "actor.system.derived.speed"; + + case "pf1": + case "D35E": return "actor.system.attributes.speed.land.total"; + case "shadowrun5e": return "actor.system.movement.walk.value"; + case "swade": return "actor.system.stats.speed.adjusted"; + case "ds4": return "actor.system.combatValues.movement.total"; + case "splittermond": return "actor.derivedValues.speed.value"; + case "wfrp4e": return "actor.system.details.move.walk"; + case "crucible": return "actor.system.movement.stride"; + case "dragonbane": return "actor.system.movement"; + case "twodsix": return "actor.system.movement.walk"; + case "worldofdarkness": return "actor.system.movement.walk"; + default: return ""; + } +} + +/** + * Location of the flying attribute for a given system's actor. + * @returns {string} + */ +export function defaultFlyAttribute() { + switch ( game.system.id ) { + // Missing attribute case "CoC7": + // Missing attribute case "dcc": + case "sfrpg": return "actor.system.attributes.flying.value"; + // Missing attribute case "dnd4e": + case "dnd5e": return "actor.system.attributes.movement.fly"; + // Missing attribute case "lancer": + case "pf1": + case "D35E": return "actor.system.attributes.speed.fly.total"; + // Missing attribute case "shadowrun5e": + // Missing attribute case "swade": + // Missing attribute case "ds4": + // Missing attribute case "splittermond": + // Missing attribute case "wfrp4e": + // Missing attribute case "crucible": + // Missing attribute case "dragonbane": + case "twodsix": return "actor.system.movement.fly"; + case "worldofdarkness": return "actor.system.movement.fly"; + default: return ""; + } +} + +/** + * Location of the burrow attribute for a given system's actor. + * @returns {string} + */ +export function defaultBurrowAttribute() { + switch ( game.system.id ) { + // Missing attribute case "CoC7": + // Missing attribute case "dcc": + case "sfrpg": return "actor.system.attributes.burrowing.value"; + // Missing attribute case "dnd4e": + case "dnd5e": return "actor.system.attributes.movement.burrow"; + // Missing attribute case "lancer": + case "pf1": + case "D35E": return "actor.system.attributes.speed.burrow.total"; + // Missing attribute case "shadowrun5e": + // Missing attribute case "swade": + // Missing attribute case "ds4": + // Missing attribute case "splittermond": + // Missing attribute case "wfrp4e": + // Missing attribute case "crucible": + // Missing attribute case "dragonbane": + case "twodsix": return "actor.system.movement.burrow"; + default: return ""; + } +} + +/** + * How much faster is dashing than walking for a given system? + * @returns {number} + */ +export function defaultDashMultiplier() { + switch ( game.system.id ) { + case "dcc": + case "dnd4e": + case "dnd5e": + case "lancer": + case "pf1": + case "D35E": + case "sfrpg": + case "shadowrun5e": + case "dragonbane": + case "twodsix": + case "ds4": return 2; + + case "CoC7": return 5; + case "splittermond": return 3; + case "wfrp4e": return 2; + + case "crucible": + case "swade": return 0; + default: return 0; + } +} From 680882dbfe8979e146a989cb44bdb1c5c6b284e1 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 11:23:32 -0700 Subject: [PATCH 19/23] =?UTF-8?q?=F0=9F=8E=B8=20feat|Speed|Add=20ARS=20spe?= =?UTF-8?q?ed=20definition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes issue #111. --- scripts/system_attributes.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/system_attributes.js b/scripts/system_attributes.js index 7a54c24..1a93c3a 100644 --- a/scripts/system_attributes.js +++ b/scripts/system_attributes.js @@ -84,6 +84,7 @@ export function defaultHPAttribute() { case "dnd5e": return "actor.system.attributes.hp.value"; case "dragonbane": return "actor.system.hitpoints.value"; case "twodsix": return "actor.system.hits.value"; + case "ars": return "actor.system.attributes.hp.value"; default: return "actor.system.attributes.hp.value"; } } @@ -94,6 +95,7 @@ export function defaultHPAttribute() { */ export function defaultWalkAttribute() { switch ( game.system.id ) { + case "ars": return "actor.movement"; case "CoC7": return "actor.system.attribs.mov.value"; case "dcc": return "actor.system.attributes.speed.value"; case "sfrpg": return "actor.system.attributes.speed.value"; From 54efb51492e04f44fa680600c7e51dacc38e7803 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 12:29:10 -0700 Subject: [PATCH 20/23] =?UTF-8?q?=F0=9F=8E=B8=20feat=20|Speed|Speed=20defi?= =?UTF-8?q?nitions=20for=20a5e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/system_attributes.js | 50 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/scripts/system_attributes.js b/scripts/system_attributes.js index 1a93c3a..5b479da 100644 --- a/scripts/system_attributes.js +++ b/scripts/system_attributes.js @@ -44,6 +44,10 @@ Hooks.once("init", function() { SPEED.ATTRIBUTES.FLY = defaultFlyAttribute(); DashSpeedCategory.multiplier = defaultDashMultiplier(); SPEED.CATEGORIES = [WalkSpeedCategory, DashSpeedCategory, MaximumSpeedCategory]; + + // Add specialized system categories + const moveCategoryFn = SPECIALIZED_MOVE_CATEGORIES[game.system.id]; + if ( moveCategoryFn ) moveCategoryFn(); }); // ----- NOTE: Attributes ----- // @@ -85,6 +89,7 @@ export function defaultHPAttribute() { case "dragonbane": return "actor.system.hitpoints.value"; case "twodsix": return "actor.system.hits.value"; case "ars": return "actor.system.attributes.hp.value"; + case "a5e": return "actor.system.attributes.hp.value"; default: return "actor.system.attributes.hp.value"; } } @@ -95,6 +100,7 @@ export function defaultHPAttribute() { */ export function defaultWalkAttribute() { switch ( game.system.id ) { + case "a5e": return "actor.system.attributes.movement.walk.distance"; case "ars": return "actor.movement"; case "CoC7": return "actor.system.attribs.mov.value"; case "dcc": return "actor.system.attributes.speed.value"; @@ -124,21 +130,11 @@ export function defaultWalkAttribute() { */ export function defaultFlyAttribute() { switch ( game.system.id ) { - // Missing attribute case "CoC7": - // Missing attribute case "dcc": + case "a5e": return "actor.system.attributes.movement.fly.distance"; case "sfrpg": return "actor.system.attributes.flying.value"; - // Missing attribute case "dnd4e": case "dnd5e": return "actor.system.attributes.movement.fly"; - // Missing attribute case "lancer": case "pf1": case "D35E": return "actor.system.attributes.speed.fly.total"; - // Missing attribute case "shadowrun5e": - // Missing attribute case "swade": - // Missing attribute case "ds4": - // Missing attribute case "splittermond": - // Missing attribute case "wfrp4e": - // Missing attribute case "crucible": - // Missing attribute case "dragonbane": case "twodsix": return "actor.system.movement.fly"; case "worldofdarkness": return "actor.system.movement.fly"; default: return ""; @@ -151,21 +147,11 @@ export function defaultFlyAttribute() { */ export function defaultBurrowAttribute() { switch ( game.system.id ) { - // Missing attribute case "CoC7": - // Missing attribute case "dcc": + case "a5e": return "actor.system.attributes.movement.burrow.distance"; case "sfrpg": return "actor.system.attributes.burrowing.value"; - // Missing attribute case "dnd4e": case "dnd5e": return "actor.system.attributes.movement.burrow"; - // Missing attribute case "lancer": case "pf1": case "D35E": return "actor.system.attributes.speed.burrow.total"; - // Missing attribute case "shadowrun5e": - // Missing attribute case "swade": - // Missing attribute case "ds4": - // Missing attribute case "splittermond": - // Missing attribute case "wfrp4e": - // Missing attribute case "crucible": - // Missing attribute case "dragonbane": case "twodsix": return "actor.system.movement.burrow"; default: return ""; } @@ -187,6 +173,7 @@ export function defaultDashMultiplier() { case "shadowrun5e": case "dragonbane": case "twodsix": + case "a5e": case "ds4": return 2; case "CoC7": return 5; @@ -198,3 +185,22 @@ export function defaultDashMultiplier() { default: return 0; } } + +// ----- Specialized move categories by system ----- // +/** + * Dnd5e Level Up (a5e) + */ +function a5eMoveCategories() { + DashSpeedCategory.name = "Action Dash"; + const BonusDashCategory = { + name: "Bonus Dash", + color: Color.from(0xf77926), + multiplier: 4 + } + SPEED.CATEGORIES = [WalkSpeedCategory, DashSpeedCategory, BonusDashCategory, MaximumSpeedCategory]; +} + +const SPECIALIZED_MOVE_CATEGORIES = { + a5e: a5eMoveCategories +}; + From 711bf097b0e8b4e33d472811d1189014eca9649a Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 13:04:06 -0700 Subject: [PATCH 21/23] =?UTF-8?q?=F0=9F=8E=B8=20feat=20|Speed|Speed=20defi?= =?UTF-8?q?nitions=20for=20sfrpg?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/system_attributes.js | 165 ++++++++++++++++++++++++++++++----- 1 file changed, 141 insertions(+), 24 deletions(-) diff --git a/scripts/system_attributes.js b/scripts/system_attributes.js index 5b479da..a9f0f06 100644 --- a/scripts/system_attributes.js +++ b/scripts/system_attributes.js @@ -1,5 +1,6 @@ /* globals Color, +foundry, game, Hooks */ @@ -48,6 +49,14 @@ Hooks.once("init", function() { // Add specialized system categories const moveCategoryFn = SPECIALIZED_MOVE_CATEGORIES[game.system.id]; if ( moveCategoryFn ) moveCategoryFn(); + + // Add specialized category distance function + const categoryDistanceFn = SPECIALIZED_CATEGORY_DISTANCE[game.system.id]; + if ( categoryDistanceFn ) SPEED.maximumCategoryDistance = categoryDistanceFn; + + // Add specialized token speed function + const tokenSpeedFn = SPECIALIZED_TOKEN_SPEED[game.system.id]; + if ( tokenSpeedFn ) SPEED.tokenSpeed = tokenSpeedFn; }); // ----- NOTE: Attributes ----- // @@ -55,29 +64,6 @@ Hooks.once("init", function() { /** * Some of below taken from Drag Ruler */ -/* -MIT License - -Copyright (c) 2021 Manuel Vögele - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ /** * Location of the HP attribute for a given system's actor. @@ -200,7 +186,138 @@ function a5eMoveCategories() { SPEED.CATEGORIES = [WalkSpeedCategory, DashSpeedCategory, BonusDashCategory, MaximumSpeedCategory]; } +/** + * sfrpg + */ +function sfrpgMoveCategories() { + WalkSpeedCategory.name = "sfrpg.speeds.walk"; + DashSpeedCategory.name = "sfrpg.speeds.dash"; + const RunSpeedCategory = { + name: "sfrpg.speeds.run", + color: Color.from(0xff8000), + multiplier: 4 + } + SPEED.CATEGORIES = [WalkSpeedCategory, DashSpeedCategory, RunSpeedCategory, MaximumSpeedCategory]; +} + const SPECIALIZED_MOVE_CATEGORIES = { - a5e: a5eMoveCategories + a5e: a5eMoveCategories, + sfrpg: sfrpgMoveCategories +}; + +// ----- Specialized token speed by system ----- // + +/** + * Given a token, retrieve its base speed. + * @param {Token} token Token whose speed is required + * @returns {number|null} Distance, in grid units. Null if no speed provided for that category. + * (Null will disable speed highlighting.) + */ +function sfrpgTokenSpeed(token) { + let speed = foundry.utils.getProperty(token, SPEED.ATTRIBUTES[token.movementType]); + switch ( token.actor?.type ) { + case "starship": speed = foundry.utils.getProperty(token, "actor.system.attributes.speed.value"); break; + case "vehicle": speed = foundry.utils.getProperty(token, "actor.system.attributes.speed.drive"); break; + } + if ( speed == null ) return null; + return Number(speed); +} + +const SPECIALIZED_TOKEN_SPEED = { + sfrpg: sfrpgTokenSpeed +}; + +// ----- Specialized category distances by system ----- // + +/** + * Starfinder (sfrpg) + * Player Characters, Drones, and Non-player Characters: + * There are three speed thresholds: single move (speed * 1), double move (speed *2), and run (speed *4) + * Vehicles: There are three speed thresholds: drive speed, run over speed (drive speed *2), and full speed + * Starships: There are two speed thresholds: normal speed, and full power (speed * 1.5) + + * + * @param {Token} token Token whose speed should be used + * @param {SpeedCategory} speedCategory Category for which the maximum distance is desired + * @param {number} [tokenSpeed] Optional token speed to avoid repeated lookups + * @returns {number} + */ +function sfrpgCategoryDistance(token, speedCategory, tokenSpeed) { + // Set default speed. + tokenSpeed ??= SPEED.tokenSpeed(token); + const type = token.actor?.type; + let speed = speedCategory.multiplier * tokenSpeed; + + // Override default speed for certain vehicles. + switch ( speedCategory.name ) { + case "sfrpg.speeds.dash": { + if ( type === "starship" ) speed = tokenSpeed * 1.5; + break; + } + + case "sfrpg.speeds.run": { + if ( type === "starship" ) speed = 0; + if ( type === "vehicle" ) speed = foundry.utils.getProperty(token, "actor.system.attributes.speed.full"); + break; + } + } + return speed; +} + +const SPECIALIZED_CATEGORY_DISTANCE = { + sfrpg: sfrpgCategoryDistance }; + + +// ----- Note: Licenses / Credits ----- // + +/* Drag Ruler +https://github.com/manuelVo/foundryvtt-drag-ruler +MIT License + +Copyright (c) 2021 Manuel Vögele + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* sfrpg +https://github.com/J-Dawe/starfinder-drag-ruler/blob/main/scripts/main.js +MIT License + +Copyright (c) 2021 J-Dawe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ From 497e7f960c52c97c56281f015eb22063d7f0fc6c Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 13:08:45 -0700 Subject: [PATCH 22/23] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20docs|Changelog|Add?= =?UTF-8?q?=20speed=20definitions=20notes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Changelog.md b/Changelog.md index 3487e2d..59d2bc0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,10 @@ For dnd5e, remove the Token HUD control to select movement, because dnd5e now us Updated Polish translation. Thanks @Lioheart! Closes #105. Added `SCENE.BACKGROUND` to flags (imported from Terrain Mapper). Closes #107. Don't broadcast the Token Ruler if the token being dragged is secret, invisible, or hidden. Closes #112. +Consolidate speed attributes to a single file. +Add ARS speed definitions. Closes #111. +Add sfrpg speed definitions. +Add a5e speed definitions. # 0.9.5 Added Brazilian translation. Thanks @Kharmans! From 191b98513fc8adf4c170d80292808b99ff3f6202 Mon Sep 17 00:00:00 2001 From: Michael Enion Date: Sun, 30 Jun 2024 13:11:55 -0700 Subject: [PATCH 23/23] =?UTF-8?q?=F0=9F=8F=B9=20release|Module.json|Update?= =?UTF-8?q?=20verified=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module.json b/module.json index 7f57d23..ac4e78c 100644 --- a/module.json +++ b/module.json @@ -8,7 +8,7 @@ "manifestPlusVersion": "1.0.0", "compatibility": { "minimum": "12", - "verified": "12.325" + "verified": "12.328" }, "authors": [ {