Compare commits

..

8 Commits

Author SHA1 Message Date
Manuel Vögele 0588d3ef8b Pathfinding prerelease 2021-07-19 22:40:02 +02:00
Manuel Vögele b084d25cb6 Add an option to do pathfinding automatically 2021-07-19 22:39:48 +02:00
Manuel Vögele 654847563f Reduce broken caching 2021-07-19 22:04:25 +02:00
Manuel Vögele baca42a2f8 Broadcast pathfinding waypoints to other players 2021-07-19 22:04:25 +02:00
Manuel Vögele 68ee098e61 Penalize diagonals minimally to disincentivise using unnecessary diagonals 2021-07-19 22:04:25 +02:00
Manuel Vögele 3300007f93 Initial working pathfinding impl 2021-07-19 22:04:25 +02:00
Manuel Vögele 07d0c37fe9 Release v1.7.7 2021-07-08 22:02:57 +02:00
Manuel Vögele b8b97ac27a Update generic speed provider default for swade (resolves #93) 2021-07-08 22:01:29 +02:00
10 changed files with 170 additions and 8 deletions
+5
View File
@@ -1,3 +1,8 @@
## 1.7.7
### Compatibility
- Updated the default settings for the swade game system. The new default speed attribute points to a speed value that gets adjusted for wounds.
## 1.7.6
### Translation
- Added Korean translation (thanks to KLO#1490)
+4
View File
@@ -34,6 +34,10 @@
"name": "Show PC speed to everyone",
"hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet."
},
"autoPathfinding": {
"name": "Pathfinding by default",
"hint": "If enabled, dragging a token will automatically use a pathfinding ruler"
},
"enableMovementHistory": {
"name": "Enable movement history during combat",
"hint": "If enabled, Drag Ruler will remember the path a token took during it's turn in combat and will display it when you pick the token back up."
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "drag-ruler",
"title": "Drag Ruler",
"description": "When dragging a token displays a ruler showing how far you've moved that token.",
"version": "1.7.6",
"version": "1.7.7-prerelease1",
"minimumCoreVersion" : "0.8.5",
"compatibleCoreVersion" : "0.8.8",
"authors": [
@@ -59,7 +59,7 @@
],
"socket": true,
"url": "https://github.com/manuelVo/foundryvtt-drag-ruler",
"download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/v1.7.6.zip",
"download": "https://codeload.github.com/manuelVo/foundryvtt-drag-ruler/zip/refs/heads/prerelease",
"manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json",
"readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md",
"changelog": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/CHANGELOG.md",
+18
View File
@@ -16,3 +16,21 @@ export function getGridPositionFromPixels(xPixel, yPixel) {
return [y, x]
return [x, y]
}
export function getGridPositionFromPixelsObj(o) {
const r = {};
[r.x, r.y] = getGridPositionFromPixels(o.x, o.y);
return r;
}
export function getPixelsFromGridPositionObj(o) {
const r = {};
[r.x, r.y] = getPixelsFromGridPosition(o.x, o.y);
return r;
}
export function getCenterFromGridPositionObj(o) {
const r = getPixelsFromGridPositionObj(o);
[r.x, r.y] = canvas.grid.getCenter(r.x, r.y);
return r;
}
+24 -3
View File
@@ -1,8 +1,10 @@
import {highlightMeasurementTerrainRuler, measureDistances} from "./compatibility.js";
import {getGridPositionFromPixels} from "./foundry_fixes.js";
import {getCenterFromGridPositionObj, getGridPositionFromPixels, getGridPositionFromPixelsObj} from "./foundry_fixes.js";
import {Line} from "./geometry.js";
import {getColorForDistance} from "./main.js"
import {trackRays} from "./movement_tracking.js"
import {find_path} from "./pathfinding.js";
import {settingsKey} from "./settings.js";
import {recalculate} from "./socket.js";
import {applyTokenSizeOffset, getSnapPointForEntity, getSnapPointForToken, getTokenShape, highlightTokenShape, zip} from "./util.js";
@@ -77,6 +79,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
this.cancelMovement = false;
for (let i = startWaypoint;i < entityAnimationData[0].rays.length; i++) {
if (!wasPaused && game.paused) break;
const entityPaths = entityAnimationData.map(({entity, rays, dx, dy}) => {
const ray = rays[i];
@@ -101,6 +104,7 @@ async function animateEntities(entities, draggedEntity, draggedRays, wasPaused)
trackRays(entities, entityAnimationData.map(({rays}) => rays)).then(() => recalculate(entities));
}
function calculateEntityOffset(entityA, entityB) {
return {x: entityA.data.x - entityB.data.x, y: entityA.data.y - entityB.data.y};
}
@@ -116,6 +120,7 @@ export function onMouseMove(event) {
if (this._state === Ruler.STATES.MOVING) return;
// Extract event data
const destination = {x: event.data.destination.x + this.rulerOffset.x, y: event.data.destination.y + this.rulerOffset.y}
// Hide any existing Token HUD
@@ -131,7 +136,7 @@ function scheduleMeasurement(destination, event) {
const mt = event._measureTime || 0;
const originalEvent = event.data.originalEvent;
if (Date.now() - mt > measurementInterval) {
this.measure(destination, {snap: !originalEvent.shiftKey});
this.measure(destination, {snap: !originalEvent.shiftKey, pathfinding: game.settings.get(settingsKey, "autoPathfinding") != game.keyboard.isDown("y")});
event._measureTime = Date.now();
this._state = Ruler.STATES.MEASURING;
window.clearTimeout(this.deferredMeasurementTimeout);
@@ -148,7 +153,7 @@ function scheduleMeasurement(destination, event) {
}
// This is a modified version of Ruler.measure form foundry 0.7.9
export function measure(destination, {gridSpaces=true, snap=false} = {}) {
export function measure(destination, {gridSpaces=true, snap=false, pathfinding=false} = {}) {
const isToken = this.draggedEntity instanceof Token;
if (isToken && !this.draggedEntity.isVisible)
return []
@@ -157,6 +162,22 @@ export function measure(destination, {gridSpaces=true, snap=false} = {}) {
destination = getSnapPointForEntity(destination.x, destination.y, this.draggedEntity);
}
// Remove waypoints generated by pathfinding
this.waypoints = this.waypoints.filter(waypoint => !waypoint.isPathfinding);
if (isToken && pathfinding) {
let path = find_path(getGridPositionFromPixelsObj(this.waypoints[this.waypoints.length - 1]), getGridPositionFromPixelsObj(destination));
if (path) {
path = path.map(point => getCenterFromGridPositionObj(point));
path.forEach(point => point.isPathfinding = true);
this.waypoints = this.waypoints.concat(path);
}
else {
// Don't show a path if the pathfinding yields no result to show the user that the destination is unreachable
destination = this.waypoints[this.waypoints.length - 1];
}
}
const terrainRulerAvailable = isToken && game.modules.get("terrain-ruler")?.active && (!game.modules.get("TerrainLayer")?.active || canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS);
const waypoints = this.waypoints.concat([destination]);
+2
View File
@@ -9,6 +9,7 @@ import {getMovementHistory, resetMovementHistory} from "./movement_tracking.js";
import {registerSettings, settingsKey} from "./settings.js"
import {recalculate} from "./socket.js";
import {SpeedProvider} from "./speed_provider.js"
import { wipe_cache } from "./pathfinding.js";
Hooks.once("init", () => {
registerSettings()
@@ -137,6 +138,7 @@ function onEntityLeftDragStart(event) {
else
entityCenter = this.center;
ruler.clear();
wipe_cache();
ruler._state = Ruler.STATES.STARTING;
ruler.rulerOffset = {x: entityCenter.x - event.data.origin.x, y: entityCenter.y - event.data.origin.y};
if (isToken && game.settings.get(settingsKey, "enableMovementHistory"))
+98
View File
@@ -0,0 +1,98 @@
import {getCenterFromGridPositionObj} from "./foundry_fixes.js";
// TODO Wipe cache if walls layer is being modified
let cached_nodes = undefined;
function get_node(pos, initialize=true) {
if (!cached_nodes)
// TODO Check if ceil is the right thing to do here
cached_nodes = new Array(Math.ceil(canvas.dimensions.sceneHeight / canvas.dimensions.size));
if (!cached_nodes[pos.y])
cached_nodes[pos.y] = new Array(Math.ceil(canvas.dimensions.sceneWidth / canvas.dimensions.size));
if (!cached_nodes[pos.y][pos.x]) {
cached_nodes[pos.y][pos.x] = {x: pos.x, y: pos.y};
}
const node = cached_nodes[pos.y][pos.x];
if (initialize && !node.edges) {
node.edges = [];
for (const neighborPos of neighbors(pos)) {
// TODO Work with pixels instead of grid locations
if (!canvas.walls.checkCollision(new Ray(getCenterFromGridPositionObj(pos), getCenterFromGridPositionObj(neighborPos)))) {
const neighbor = get_node(neighborPos, false);
node.edges.push({target: neighbor, cost: 1});
}
}
}
return node;
}
function* neighbors(pos) {
for (let y = -1;y < 2;y++) {
for (let x = -1;x < 2;x++) {
if (x != 0 || y != 0)
yield {x: pos.x + x, y: pos.y + y};
}
}
}
function calculate_path(from, to) {
const nextNodes = [{node: get_node(to), cost: 0, estimated: estimate_cost(to, from), previous: null}];
const previousNodes = new Set();
while (nextNodes.length > 0) {
// Sort by estimated cost, high to low
// TODO Re-sorting every iteration is expensive. Think of something better
nextNodes.sort((a, b) => b.estimated - a.estimated);
// Get node with cheapest estimate
const currentNode = nextNodes.pop();
if (currentNode.node.x === from.x && currentNode.node.y === from.y)
return currentNode;
previousNodes.add(currentNode.node);
for (const edge of currentNode.node.edges) {
const neighborNode = get_node(edge.target);
if (previousNodes.has(neighborNode))
continue;
// TODO We currently assume a cost of one for all transitions. Change this for 5/10/5 or difficult terrain support
// We charge an extra 0.0001 for diagonals
const isDiagonal = currentNode.node.x !== neighborNode.x && currentNode.node.y !== neighborNode.y;
const edgeCost = isDiagonal ? 1.0001 : 1;
const neighbor = {node: neighborNode, cost: currentNode.cost + edgeCost, estimated: currentNode.cost + edgeCost + estimate_cost(neighborNode, from), previous: currentNode};
const neighborIndex = nextNodes.findIndex(node => node.node === neighbor.node);
if (neighborIndex >= 0) {
// If the neighbor is cheaper to reach via the current route than through previously discovered routes, replace it
if (nextNodes[neighborIndex].cost > neighbor.cost) {
nextNodes[neighborIndex] = neighbor;
}
}
else {
nextNodes.push(neighbor);
}
}
}
}
function estimate_cost(pos, target) {
return Math.max(Math.abs(pos.x - target.x), Math.abs(pos.y - target.y));
}
export function find_path(from, to) {
const lastNode = calculate_path(from, to);
if (!lastNode)
return null;
const path = [];
let currentNode = lastNode;
while (currentNode) {
// TODO Check if the distance doesn't change
if (path.length >= 2 && !canvas.walls.checkCollision(new Ray(getCenterFromGridPositionObj(currentNode.node), getCenterFromGridPositionObj(path[path.length - 2]))))
// Replace last waypoint if the current waypoint leads to a valid path
path[path.length - 1] = {x: currentNode.node.x, y: currentNode.node.y};
else
path.push({x: currentNode.node.x, y: currentNode.node.y});
currentNode = currentNode.previous;
}
return path;
}
export function wipe_cache() {
cached_nodes = undefined;
}
+7 -2
View File
@@ -35,8 +35,13 @@ export class DragRulerRuler extends Ruler {
const json = super.toJSON();
if (this.draggedEntity) {
const isToken = this.draggedEntity instanceof Token;
json["draggedEntityIsToken"] = isToken;
json["draggedEntity"] = this.draggedEntity.id;
json.draggedEntityIsToken = isToken;
json.draggedEntity = this.draggedEntity.id;
json.waypoints = json.waypoints.map(old => {
let w = duplicate(old);
w.isPathfinding = undefined;
return w;
});
}
return json;
}
+9
View File
@@ -47,6 +47,15 @@ export function registerSettings() {
default: true,
});
game.settings.register(settingsKey, "autoPathfinding", {
name: "drag-ruler.settings.autoPathfinding.name",
hint: "drag-ruler.settings.autoPathfinding.hint",
scpoe: "client",
config: true,
type: Boolean,
defualt: true,
});
game.settings.register(settingsKey, "lastTerrainRulerHintTime", {
config: false,
type: Number,
+1 -1
View File
@@ -17,7 +17,7 @@ export function getDefaultSpeedAttribute() {
case "shadowrun5e":
return "actor.data.data.movement.walk.value";
case "swade":
return "actor.data.data.stats.speed.value"
return "actor.data.data.stats.speed.adjusted";
}
return ""
}