From c66ec46aed8bb6f04887bc41592f5e8a9c6bad2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20V=C3=B6gele?= Date: Fri, 2 Apr 2021 23:24:48 +0200 Subject: [PATCH] Track the path taken by tokens in combat and display it when reactivating DragRuler in the same turn --- src/compatibility.js | 4 +-- src/foundry_imports.js | 30 ++++++++++++------- src/main.js | 6 ++-- src/movement_tracking.js | 62 ++++++++++++++++++++++++++++++++++++++++ src/ruler.js | 37 ++++++++++++++++++++++-- src/util.js | 7 +++-- 6 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 src/movement_tracking.js diff --git a/src/compatibility.js b/src/compatibility.js index e40e362..a0f7343 100644 --- a/src/compatibility.js +++ b/src/compatibility.js @@ -6,9 +6,9 @@ export function getHexSizeSupportTokenGridCenter(token) { return {x: token.x + tokenCenterOffset.x, y: token.y + tokenCenterOffset.y} } -export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape=[{x: 0, y: 0}]) { +export function highlightMeasurementTerrainRuler(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) { for (const space of ray.terrainRulerVisitedSpaces.reverse()) { const color = getColorForDistance.call(this, startDistance, space.distance) - highlightTokenShape.call(this, space, tokenShape, color) + highlightTokenShape.call(this, space, tokenShape, color, alpha) } } diff --git a/src/foundry_imports.js b/src/foundry_imports.js index 2d5d660..f2bd6c3 100644 --- a/src/foundry_imports.js +++ b/src/foundry_imports.js @@ -2,6 +2,7 @@ import { getCostFromSpeedProvider } from "./api.js"; import {highlightMeasurementTerrainRuler} from "./compatibility.js"; import {getGridPositionFromPixels} from "./foundry_fixes.js"; import {getColorForDistance} from "./main.js" +import {trackRays} from "./movement_tracking.js" import {applyTokenSizeOffset, getAreaFromPositionAndShape, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js"; // This is a modified version of Ruler.moveToken from foundry 0.7.9 @@ -16,7 +17,7 @@ export async function moveTokens(draggedToken, selectedTokens) { // Get the movement rays and check collision along each Ray // These rays are center-to-center for the purposes of collision checking - const rays = this._getRaysFromWaypoints(this.waypoints, this.destination); + const rays = this.dragRulerGetRaysFromWaypoints(this.waypoints, this.destination); if (!game.user.isGM) { const hasCollision = selectedTokens.some(token => { const offset = calculateTokenOffset(token, draggedToken) @@ -45,11 +46,13 @@ export async function moveTokens(draggedToken, selectedTokens) { // This is a modified version code extracted from Ruler.moveToken from foundry 0.7.9 async function animateToken(token, rays, tokenOffset, wasPaused) { - const offsetRays = rays.map(ray => applyOffsetToRay(ray, tokenOffset)) + const offsetRays = rays.filter(r => !r.isPrevious).map(ray => applyOffsetToRay(ray, tokenOffset)); + trackRays(token, offsetRays); // Determine offset relative to the Token top-left. // This is important so we can position the token relative to the ruler origin for non-1x1 tokens. - const origin = [this.waypoints[0].x + tokenOffset.x, this.waypoints[0].y + tokenOffset.y] + const firstWaypoint = this.waypoints.find(w => !w.isPrevious); + const origin = [firstWaypoint.x + tokenOffset.x, firstWaypoint.y + tokenOffset.y]; let dx, dy if (canvas.grid.type === CONST.GRID_TYPES.GRIDLESS) { dx = token.data.x - origin[0] @@ -76,7 +79,9 @@ function calculateTokenOffset(tokenA, tokenB) { } function applyOffsetToRay(ray, offset) { - return new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y}) + const newRay = new Ray({x: ray.A.x + offset.x, y: ray.A.y + offset.y}, {x: ray.B.x + offset.x, y: ray.B.y + offset.y}); + newRay.isPrevious = ray.isPrevious; + return newRay; } // This is a modified version of Ruler._onMouseMove from foundry 0.7.9 @@ -130,6 +135,8 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { const label = this.labels.children[i]; const ray = new Ray(origin, dest); const centeredRay = new Ray(centeredOrigin, centeredDest) + ray.isPrevious = Boolean(origin.isPrevious); + centeredRay.isPrevious = ray.isPrevious; if (ray.distance < 10) { if (label) label.visible = false; continue; @@ -173,8 +180,9 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { const { label, text, last } = cs; // Draw line segment - r.lineStyle(6, 0x000000, 0.5).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y) - .lineStyle(4, rulerColor, 0.25).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y); + const opacityMultiplier = s.ray.isPrevious ? 0.33 : 1; + r.lineStyle(6, 0x000000, 0.5 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y) + .lineStyle(4, rulerColor, 0.25 * opacityMultiplier).moveTo(s.ray.A.x, s.ray.A.y).lineTo(s.ray.B.x, s.ray.B.y); // Draw the distance label just after the endpoint of the segment if (label) { @@ -187,9 +195,9 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { // Highlight grid positions if (terrainRulerAvailable) - highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape) + highlightMeasurementTerrainRuler.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier) else - highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape); + highlightMeasurementNative.call(this, cs.ray, cs.startDistance, shape, opacityMultiplier); } // Draw endpoints @@ -201,7 +209,7 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) { return segments; } -export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0, y: 0}]) { +export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0, y: 0}], alpha=1) { const spacer = canvas.scene.data.gridType === CONST.GRID_TYPES.SQUARE ? 1.41 : 1; const nMax = Math.max(Math.floor(ray.distance / (spacer * Math.min(canvas.grid.w, canvas.grid.h))), 1); const tMax = Array.fromRange(nMax+1).map(t => t / nMax); @@ -237,9 +245,9 @@ export function highlightMeasurementNative(ray, startDistance, tokenShape=[{x: 0 const color = dragRuler.getColorForDistance.call(this, startDistance, subDistance) const snapPoint = getSnapPointForToken(...canvas.grid.getTopLeft(x, y), this.draggedToken); const [snapX, snapY] = getGridPositionFromPixels(snapPoint.x + 1, snapPoint.y + 1); - highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color); + highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha); } - highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color); + highlightTokenShape.call(this, {x: snapX, y: snapY}, tokenShape, color, alpha); } } diff --git a/src/main.js b/src/main.js index e5dea17..91f07d7 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ import {getHexSizeSupportTokenGridCenter} from "./compatibility.js" import {moveTokens, onMouseMove} from "./foundry_imports.js" import {performMigrations} from "./migration.js" import {DragRulerRuler} from "./ruler.js"; +import {getMovementHistory} from "./movement_tracking.js"; import {registerSettings, settingsKey} from "./settings.js" import {SpeedProvider} from "./speed_provider.js" @@ -120,6 +121,7 @@ function onTokenLeftDragStart(event) { ruler.clear(); ruler._state = Ruler.STATES.STARTING; ruler.rulerOffset = {x: tokenCenter.x - event.data.origin.x, y: tokenCenter.y - event.data.origin.y} + ruler.dragRulerAddWaypointHistory(getMovementHistory(this)); ruler.dragRulerAddWaypoint(tokenCenter, false); } @@ -146,9 +148,7 @@ function onTokenDragLeftCancel(event) { return false if (ruler._state === Ruler.STATES.MEASURING) { if (!game.settings.get(settingsKey, "swapSpacebarRightClick")) { - if (ruler.waypoints.length > 1) - event.preventDefault() - ruler.dragRulerDeleteWaypoint(); + ruler.dragRulerDeleteWaypoint(event); } else { event.preventDefault() diff --git a/src/movement_tracking.js b/src/movement_tracking.js new file mode 100644 index 0000000..2d1ecaf --- /dev/null +++ b/src/movement_tracking.js @@ -0,0 +1,62 @@ +function initTrackingFlag(combatant) { + const initialFlag = {passedWaypoints: [], trackedRound: 0}; + let dragRulerFlag = combatant.flags?.dragRuler; + if (dragRulerFlag) { + if (isNaN(dragRulerFlag.trackedRound)) { + mergeObject(dragRulerFlag, initialFlag); + } + } + else { + combatant.flags.dragRuler = initialFlag; + } +} + +function getInitializedCombatant(token, combat) { + const combatant = combat.getCombatantByToken(token.data._id); + if (!combatant) + return undefined; + initTrackingFlag(combatant); + return combatant; +} + +export async function trackRays(token, rays) { + // Only track movement if the current token is participating in the active combat + const combat = game.combat; + if (!combat) + return; + if (!combat.started) + return; + const combatant = getInitializedCombatant(token, combat); + if (!combatant) + return; + + // Check if we have entered a new round. If so, remove the currently stored path + if (combat.data.round > combatant.flags.dragRuler.trackedRound) { + combatant.flags.dragRuler.passedWaypoints = []; + combatant.flags.dragRuler.trackedRound = combat.data.round; + } + + // Add the passed waypoints to the combatant + const waypoints = combatant.flags.dragRuler.passedWaypoints; + for (const ray of rays) { + // Ignore rays that have the same start and end coordinates + if (ray.A.x !== ray.B.x || ray.A.y !== ray.B.y) + waypoints.push(ray.A); + } + await combat.updateEmbeddedEntity("Combatant", {_id: combatant._id, flags: combatant.flags}, {diff: false}); +} + +export function getMovementHistory(token) { + const combat = game.combat; + if (!combat) + return []; + const combatant = combat.getCombatantByToken(token.data._id); + if (!combatant) + return []; + const dragRulerFlags = combatant.flags.dragRuler; + if (!dragRulerFlags) + return []; + if (combat.data.round > dragRulerFlags.trackedRound) + return []; + return dragRulerFlags.passedWaypoints ?? []; +} diff --git a/src/ruler.js b/src/ruler.js index 28164f2..95dfa10 100644 --- a/src/ruler.js +++ b/src/ruler.js @@ -4,6 +4,18 @@ import {getSnapPointForToken} from "./util.js"; export class DragRulerRuler extends Ruler { // Functions below are overridden versions of functions in Ruler + constructor(user, {color=null}={}) { + super(user, {color}); + this.previousWaypoints = []; + this.previousLabels = this.addChild(new PIXI.Container()); + } + + clear() { + super.clear(); + this.previousWaypoints = []; + this.previousLabels.removeChildren().forEach(c => c.destroy()); + } + async moveToken(event) { // This function is invoked by left clicking if (!this.isDragRuler) @@ -57,8 +69,17 @@ export class DragRulerRuler extends Ruler { this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle)); } - dragRulerDeleteWaypoint() { - if (this.waypoints.length > 1) { + dragRulerAddWaypointHistory(waypoints) { + waypoints = waypoints.map(waypoint => {return {x: waypoint.x, y: waypoint.y, isPrevious: true}}); + this.waypoints = this.waypoints.concat(waypoints); + for (const waypoint of waypoints) { + this.labels.addChild(new PreciseText("", CONFIG.canvasTextStyle)); + } + } + + dragRulerDeleteWaypoint(event={preventDefault: () => {return}}) { + if (this.waypoints.filter(w => !w.isPrevious).length > 1) { + event.preventDefault(); const mousePosition = canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens); const rulerOffset = this.rulerOffset; this._removeWaypoint({x: mousePosition.x + rulerOffset.x, y: mousePosition.y + rulerOffset.y}); @@ -74,7 +95,17 @@ export class DragRulerRuler extends Ruler { // This will cancel the current drag operation // Pass in a fake event that hopefully is enough to allow other modules to function - token._onDragLeftCancel({preventDefault: () => {return}}); + token._onDragLeftCancel(event); } } + + dragRulerGetRaysFromWaypoints(waypoints, destination) { + if ( destination ) + waypoints = waypoints.concat([destination]); + return waypoints.slice(1).map((wp, i) => { + const ray = new Ray(waypoints[i], wp); + ray.isPrevious = Boolean(waypoints[i].isPrevious); + return ray; + }); + } } diff --git a/src/util.js b/src/util.js index dd284b0..6f63beb 100644 --- a/src/util.js +++ b/src/util.js @@ -39,11 +39,14 @@ export function getSnapPointForToken(x, y, token) { return new PIXI.Point(snappedX + canvas.grid.w / 2, snappedY + canvas.grid.h / 2) } -export function highlightTokenShape(position, shape, color) { +export function highlightTokenShape(position, shape, color, alpha) { + const layer = canvas.grid.highlightLayers[this.name]; + if ( !layer ) + return false; const area = getAreaFromPositionAndShape(position, shape); for (const space of area) { const [x, y] = getPixelsFromGridPosition(space.x, space.y); - canvas.grid.highlightPosition(this.name, {x, y, color}) + canvas.grid.grid.highlightGridPosition(layer, {x, y, color, alpha: 0.25 * alpha}); } }