diff --git a/Changelog.md b/Changelog.md index acd41b3..a538634 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,7 @@ +# 0.7.5 +Fix for pause/unpause game not working due to conflict with token move using spacebar. +Add handling for unsnapping from the grid. If shift is held, unsnap ruler waypoints and destination. If measuring from a token, set the origin point of the ruler to the (possibly unsnapped) token center. + # 0.7.4 Add option to enable a Token Ruler when dragging tokens. Add option to use a token speed attribute to highlight in differing colors when using the ruler (from a token) or dragging tokens. diff --git a/scripts/BaseGrid.js b/scripts/BaseGrid.js new file mode 100644 index 0000000..eb39526 --- /dev/null +++ b/scripts/BaseGrid.js @@ -0,0 +1,22 @@ +/* globals +canvas +*/ +/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ + +// Patches for the BaseGrid class +export const PATCHES = {}; +PATCHES.BASIC = {}; + +/** + * Mix BaseGrid.prototype._getRulerDestination + * If the ruler is not snapped, then return the actual token position, adjusted for token dimensions. + * @param {Ray} ray The ray being moved along. + * @param {Point} offset The offset of the ruler's origin relative to the token's position. + * @param {Token} token The token placeable being moved. + */ +function _getRulerDestination(wrapped, ray, offset, token) { + if ( canvas.controls.ruler._unsnap ) return ray.B.add(offset); + return wrapped(ray, offset, token); +} + +PATCHES.BASIC.MIXES = { _getRulerDestination }; diff --git a/scripts/ClientKeybindings.js b/scripts/ClientKeybindings.js index 98e839b..50c7849 100644 --- a/scripts/ClientKeybindings.js +++ b/scripts/ClientKeybindings.js @@ -15,18 +15,20 @@ PATCHES.TOKEN_RULER = {}; // Assume this patch is only present if the token rule * If the Token Ruler is active, call that instead. * @param {KeyboardEventContext} context The context data of the event */ -async function _onMeasuredRulerMovement(wrapped, context) { - console.log("_onMeasuredRulerMovement"); +function _onMeasuredRulerMovement(wrapped, context) { // We only care about when tokens are being dragged const ruler = canvas.controls.ruler; + if ( ui.controls.tool !== "select" ) return wrapped(context); + + // If in token selection, don't use the ruler unless we are already starting a measurement. if ( !ruler.active - || !canvas.tokens.active - || ui.controls.tool !== "select" ) return wrapped(context); + || !canvas.controls.ruler._state + || !canvas.tokens.active ) return false; // For each controlled token, end the drag. canvas.tokens.clearPreviewContainer(); - await ruler.moveToken(); - ruler._endMeasurement(); + ruler.moveToken().then((response) => ruler._endMeasurement()); + return true; } PATCHES.TOKEN_RULER.STATIC_WRAPS = { _onMeasuredRulerMovement } diff --git a/scripts/HexagonalGrid.js b/scripts/HexagonalGrid.js new file mode 100644 index 0000000..7b821c6 --- /dev/null +++ b/scripts/HexagonalGrid.js @@ -0,0 +1,22 @@ +/* globals +canvas +*/ +/* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ + +// Patches for the HexagonalGrid class +export const PATCHES = {}; +PATCHES.BASIC = {}; + +/** + * Mix HexagonalGrid.prototype._getRulerDestination + * If the ruler is not snapped, then return the actual token position, adjusted for token dimensions. + * @param {Ray} ray The ray being moved along. + * @param {Point} offset The offset of the ruler's origin relative to the token's position. + * @param {Token} token The token placeable being moved. + */ +function _getRulerDestination(wrapped, ray, offset, token) { + if ( canvas.controls.ruler._unsnap ) return ray.B.add(offset); + return wrapped(ray, offset, token); +} + +PATCHES.BASIC.MIXES = { _getRulerDestination }; diff --git a/scripts/const.js b/scripts/const.js index 49634d8..50886b1 100644 --- a/scripts/const.js +++ b/scripts/const.js @@ -20,6 +20,7 @@ Hooks.once("init", function() { MODULES_ACTIVE.ENHANCED_TERRAIN_LAYER = game.modules.get("enhanced-terrain-layer")?.active; MODULES_ACTIVE.LEVELS = game.modules.get("levels")?.active; MODULES_ACTIVE.ELEVATED_VISION = game.modules.get("elevatedvision")?.active; + MODULES_ACTIVE.TERRAIN_MAPPER = game.modules.get("terrainmapper")?.active; }); /** diff --git a/scripts/patching.js b/scripts/patching.js index d405bdb..aac7dc0 100644 --- a/scripts/patching.js +++ b/scripts/patching.js @@ -10,13 +10,17 @@ import { PATCHES as PATCHES_Token } from "./Token.js"; import { PATCHES as PATCHES_GridLayer } from "./GridLayer.js"; import { PATCHES as PATCHES_PlaceableObject } from "./PlaceableObject.js"; import { PATCHES as PATCHES_ClientKeybindings } from "./ClientKeybindings.js"; +import { PATCHES as PATCHES_BaseGrid } from "./BaseGrid.js"; +import { PATCHES as PATCHES_HexagonalGrid } from "./HexagonalGrid.js"; // Settings import { PATCHES as PATCHES_Settings } from "./ModuleSettingsAbstract.js"; const PATCHES = { + BaseGrid: PATCHES_BaseGrid, ClientKeybindings: PATCHES_ClientKeybindings, GridLayer: PATCHES_GridLayer, + HexagonalGrid: PATCHES_HexagonalGrid, Ruler: PATCHES_Ruler, PlaceableObject: PATCHES_PlaceableObject, Token: PATCHES_Token, diff --git a/scripts/ruler.js b/scripts/ruler.js index 0112379..292a8aa 100644 --- a/scripts/ruler.js +++ b/scripts/ruler.js @@ -2,6 +2,7 @@ canvas, Color, CONST, +duplicate, game, getProperty, PIXI, @@ -28,9 +29,8 @@ import { _animateSegment } from "./segments.js"; -import { SPEED } from "./const.js"; +import { SPEED, MODULES_ACTIVE } from "./const.js"; import { Ray3d } from "./geometry/3d/Ray3d.js"; -import { Point3d } from "./geometry/3d/Point3d.js"; /** * Modified Ruler @@ -88,17 +88,20 @@ function toJSON(wrapper) { // console.log("constructing ruler json!") const obj = wrapper(); obj._userElevationIncrements = this._userElevationIncrements; + obj._unsnap = this._unsnap; return obj; } /** * Wrap Ruler.prototype.update - * Retrieve the current _userElevationIncrements + * Retrieve the current _userElevationIncrements. + * Retrieve the current snap status. */ function update(wrapper, data) { // Fix for displaying user elevation increments as they happen. const triggerMeasure = this._userElevationIncrements !== data._userElevationIncrements; this._userElevationIncrements = data._userElevationIncrements; + this._unsnap = data._unsnap; wrapper(data); if ( triggerMeasure ) { @@ -114,6 +117,21 @@ function update(wrapper, data) { */ function _addWaypoint(wrapper, point) { wrapper(point); + + // If moving a token, start the origin at the token center. + if ( this.waypoints.length === 1 ) { + // Temporarily replace the waypoint with the point so we can detect the token properly. + const snappedWaypoint = duplicate(this.waypoints[0]); + this.waypoints[0].copyFrom(point); + const token = this._getMovementToken(); + if ( token ) this.waypoints[0].copyFrom(token.center); + else this.waypoints[0].copyFrom(snappedWaypoint); + } + + // Otherwise if shift was held, use the precise point. + else if ( this._unsnap ) this.waypoints.at(-1).copyFrom(point); + + // Elevate the waypoint. addWaypointElevationIncrements(this, point); } @@ -127,6 +145,18 @@ function _removeWaypoint(wrapper, point, { snap = true } = {}) { wrapper(point, { snap }); } +/** + * Wrap Ruler.prototype._getMeasurementDestination + * If shift was held, use the precise destination instead of snapping. + * @param {Point} destination The current pixel coordinates of the mouse movement + * @returns {Point} The destination point, a center of a grid space + */ +function _getMeasurementDestination(wrapped, destination) { + const pt = wrapped(destination); + if ( this._unsnap ) pt.copyFrom(destination); + return pt; +} + /** * Wrap Ruler.prototype._animateMovement * Add additional controlled tokens to the move, if permitted. @@ -162,6 +192,42 @@ function dragRulerClearWaypoints(wrapper) { this._userElevationIncrements = 0; } +/** + * Wrap Ruler.prototype._computeDistance + * Add moveDistance property to each segment; track the total. + * If token not present or Terrain Mapper not active, this will be the same as segment distance. + * @param {boolean} gridSpaces Base distance on the number of grid spaces moved? + */ +function _computeDistance(wrapped, gridSpaces) { + wrapped(gridSpaces); + + // Add a movement distance based on token and terrain for the segment. + // Default to segment distance. + const token = this._getMovementToken(); + let totalMoveDistance = 0; + for ( const segment of this.segments ) { + segment.moveDistance = modifiedMoveDistance(segment.distance, segment.ray, token); + totalMoveDistance += segment.moveDistance; + } + this.totalMoveDistance = totalMoveDistance; +} + + +/** + * Modify distance by terrain mapper adjustment for token speed. + * @param {number} distance Distance of the ray + * @param {Ray|Ray3d} ray Ray to measure + * @param {Token} token Token to use + * @returns {number} Modified distance + */ +function modifiedMoveDistance(distance, ray, token) { + if ( !MODULES_ACTIVE.TERRAIN_MAPPER || !token ) return distance; + const terrainAPI = game.modules.get("terrainmapper").api; + const moveMult = terrainAPI.Terrain.percentMovementForTokenAlongPath(token, ray.A, ray.B); + if ( !moveMult ) return distance; + return distance * (1 / moveMult); // Invert because moveMult is < 1 if speed is penalized. +} + // ----- NOTE: Segment highlighting ----- // /** @@ -179,7 +245,7 @@ function _highlightMeasurementSegment(wrapped, segment) { let pastDistance = 0; for ( const s of this.segments ) { if ( s === segment ) break; - pastDistance += s.distance; + pastDistance += s.moveDistance; } // Constants @@ -189,28 +255,24 @@ function _highlightMeasurementSegment(wrapped, segment) { const dashColor = Color.from(0xffff00); const maxColor = Color.from(0xff0000); - if ( segment.distance > walkDist ) { - console.debug(`${segment.distance}`); - } - // Track the splits. let remainingSegment = segment; const splitSegments = []; // Walk remainingSegment.color = walkColor; - const walkSegments = splitSegment(remainingSegment, pastDistance, walkDist); + const walkSegments = splitSegment(remainingSegment, pastDistance, walkDist, token); if ( walkSegments.length ) { const segment0 = walkSegments[0]; splitSegments.push(segment0); - pastDistance += segment0.distance; + pastDistance += segment0.moveDistance; remainingSegment = walkSegments[1]; // May be undefined. } // Dash if ( remainingSegment ) { remainingSegment.color = dashColor; - const dashSegments = splitSegment(remainingSegment, pastDistance, dashDist); + const dashSegments = splitSegment(remainingSegment, pastDistance, dashDist, token); if ( dashSegments.length ) { const segment0 = dashSegments[0]; splitSegments.push(segment0); @@ -256,10 +318,10 @@ function _highlightMeasurementSegment(wrapped, segment) { * - If cutoffDistance is after the segment end, return [segment]. * - If cutoffDistance is within the segment, return [segment0, segment1] */ -function splitSegment(segment, pastDistance, cutoffDistance) { +function splitSegment(segment, pastDistance, cutoffDistance, token) { cutoffDistance -= pastDistance; if ( cutoffDistance <= 0 ) return []; - if ( cutoffDistance >= segment.distance ) return [segment]; + if ( cutoffDistance >= segment.moveDistance ) return [segment]; // Determine where on the segment ray the cutoff occurs. // Use canvas grid distance measurements to handle 5-5-5, 5-10-5, other measurement configs. @@ -302,11 +364,12 @@ function splitSegment(segment, pastDistance, cutoffDistance) { breakPoint.z = z; const shorterSegment = { ray: new Ray3d(A, breakPoint) }; shorterSegment.distance = canvas.grid.measureDistances([shorterSegment], { gridSpaces: true })[0]; - if ( shorterSegment.distance <= cutoffDistance ) break; + shorterSegment.moveDistance = modifiedMoveDistance(shorterSegment.distance, shorterSegment.ray, token); + if ( shorterSegment.moveDistance <= cutoffDistance ) break; } } else { // Use t values. - const t = cutoffDistance / segment.distance; + const t = cutoffDistance / segment.moveDistance; breakPoint = A.projectToward(B, t); } if ( breakPoint === B ) return [segment]; @@ -318,6 +381,8 @@ function splitSegment(segment, pastDistance, cutoffDistance) { const distances = canvas.grid.measureDistances(segments, { gridSpaces: false }); segment0.distance = distances[0]; segment1.distance = distances[1]; + segment0.moveDistance = modifiedMoveDistance(segment0.distance, segment0.ray, token); + segment1.moveDistance = modifiedMoveDistance(segment1.distance, segment1.ray, token); return segments; } @@ -396,19 +461,85 @@ export function * iterateGridUnderLine(origin, destination, { reverse = false } // await t0.scene.updateEmbeddedDocuments(t0.constructor.embeddedName, updates); // return true; +// ----- NOTE: Event handling ----- // + +/** + * Wrap Ruler.prototype._onDragStart + * Record whether shift is held. + * @param {PIXI.FederatedEvent} event The drag start event + * @see {Canvas._onDragLeftStart} + */ +function _onDragStart(wrapped, event) { + this._unsnap = event.shiftKey; + return wrapped(event); +} + +/** + * Wrap Ruler.prototype._onClickLeft. + * Record whether shift is held. + * @param {PIXI.FederatedEvent} event The pointer-down event + * @see {Canvas._onDragLeftStart} + */ +function _onClickLeft(wrapped, event) { + this._unsnap = event.shiftKey; + return wrapped(event); +} + +/** + * Wrap Ruler.prototype._onClickRight + * Record whether shift is held. + * @param {PIXI.FederatedEvent} event The pointer-down event + * @see {Canvas._onClickRight} + */ +function _onClickRight(wrapped, event) { + this._unsnap = event.shiftKey; + return wrapped(event); +} + +/** + * Wrap Ruler.prototype._onMouseMove + * Record whether shift is held. + * @param {PIXI.FederatedEvent} event The mouse move event + * @see {Canvas._onDragLeftMove} + */ +function _onMouseMove(wrapped, event) { + this._unsnap = event.shiftKey; + return wrapped(event); +} + +/** + * Wrap Ruler.prototype._onMouseUp + * Record whether shift is held + * @param {PIXI.FederatedEvent} event The pointer-up event + * @see {Canvas._onDragLeftDrop} + */ +function _onMouseUp(wrapped, event) { + this._unsnap = event.shiftKey; + return wrapped(event); +} + PATCHES.BASIC.WRAPS = { clear, toJSON, update, _addWaypoint, _removeWaypoint, + _getMeasurementDestination, // Wraps related to segments _getMeasurementSegments, _getSegmentLabel, + _computeDistance, // Move token methods - _animateMovement + _animateMovement, + + // Events + _onDragStart, + _onClickLeft, + _onClickRight, + _onMouseMove, + _onMouseUp }; PATCHES.SPEED_HIGHLIGHTING.WRAPS = { _highlightMeasurementSegment };