Skip to content

Commit ef7fcd8

Browse files
Adding an implementation of A Star pathfinding.
1 parent cfacc35 commit ef7fcd8

File tree

3 files changed

+210
-64
lines changed

3 files changed

+210
-64
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ActionType, RunePlugin } from '@server/plugins/plugin';
2+
import { commandAction } from '@server/world/actor/player/action/input-command-action';
3+
import { Position } from '@server/world/position';
4+
5+
const action: commandAction = (details) => {
6+
const { player, args } = details;
7+
8+
const x: number = args.x as number;
9+
const y: number = args.y as number;
10+
const diameter: number = args.diameter as number;
11+
12+
player.pathfinding.walkTo(new Position(x, y, player.position.level), diameter);
13+
};
14+
15+
export default new RunePlugin({
16+
type: ActionType.COMMAND,
17+
commands: [ 'path' ],
18+
args: [
19+
{
20+
name: 'x',
21+
type: 'number'
22+
},
23+
{
24+
name: 'y',
25+
type: 'number'
26+
},
27+
{
28+
name: 'diameter',
29+
type: 'number',
30+
defaultValue: 64
31+
}
32+
],
33+
action
34+
});

src/world/actor/pathfinding.ts

Lines changed: 174 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,19 @@ import { Actor } from '@server/world/actor/actor';
33
import { Position } from '../position';
44
import { Chunk } from '@server/world/map/chunk';
55
import { Tile } from '@runejs/cache-parser';
6+
import { Player } from '@server/world/actor/player/player';
67

78
class Point {
89

910
private _parent: Point = null;
10-
private _cost: number;
11-
private _heuristic: number;
12-
private _depth: number;
11+
private _cost: number = 0;
1312

14-
public constructor(private readonly _x: number, private readonly _y: number) {
15-
}
16-
17-
public compare(point: Point): number {
18-
return this._cost - point._cost;
13+
public constructor(private readonly _x: number, private readonly _y: number,
14+
public readonly indexX: number, public readonly indexY: number) {
1915
}
2016

2117
public equals(point: Point): boolean {
22-
if(this._cost === point._cost && this._heuristic === point._heuristic && this._depth === point._depth) {
18+
if(this._cost === point._cost) {
2319
if(this._parent === null && point._parent !== null) {
2420
return false;
2521
} else if(this._parent !== null && !this._parent.equals(point._parent)) {
@@ -55,23 +51,6 @@ class Point {
5551
public set cost(value: number) {
5652
this._cost = value;
5753
}
58-
59-
public get heuristic(): number {
60-
return this._heuristic;
61-
}
62-
63-
public set heuristic(value: number) {
64-
this._heuristic = value;
65-
}
66-
67-
public get depth(): number {
68-
return this._depth;
69-
}
70-
71-
public set depth(value: number) {
72-
this._depth = value;
73-
}
74-
7554
}
7655

7756
export class Pathfinding {
@@ -84,26 +63,51 @@ export class Pathfinding {
8463
public constructor(private actor: Actor) {
8564
}
8665

87-
public pathTo(destinationX: number, destinationY: number, diameter: number = 32): void {
66+
public walkTo(position: Position, pathingDiameter: number = 16): void {
67+
const path = this.pathTo(position.x, position.y, pathingDiameter);
68+
69+
if(!path) {
70+
throw new Error(`Unable to find path.`);
71+
}
72+
73+
const walkingQueue = this.actor.walkingQueue;
74+
75+
if(this.actor instanceof Player) {
76+
this.actor.walkingTo = null;
77+
}
78+
79+
walkingQueue.clear();
80+
walkingQueue.valid = true;
81+
82+
for(const point of path) {
83+
walkingQueue.add(point.x, point.y);
84+
}
85+
}
86+
87+
public pathTo(destinationX: number, destinationY: number, diameter: number = 16): Point[] {
8888
// @TODO check if destination is too far away
8989

90-
const currentPos = this.actor.position;
9190
const radius = Math.floor(diameter / 2);
92-
const startX = currentPos.x;
93-
const startY = currentPos.y;
94-
const pathingStartX = startX - radius;
95-
const pathingStartY = startY - radius;
91+
const pathingStartX = this.actor.position.x - radius;
92+
const pathingStartY = this.actor.position.y - radius;
93+
94+
if(destinationX < pathingStartX || destinationY < pathingStartY) {
95+
throw new Error(`Pathing diameter too small!`);
96+
}
97+
98+
const pointLen = diameter + 1; // + 1 for the center row & column
99+
this.points = [];
96100

97-
this.points = new Array(diameter).fill(new Array(diameter));
101+
for(let x = 0; x < pointLen; x++) {
102+
this.points.push([]);
98103

99-
for(let x = 0; x < diameter; x++) {
100-
for(let y = 0; y < diameter; y++) {
101-
this.points[x][y] = new Point(x + startX, y + startY);
104+
for(let y = 0; y < pointLen; y++) {
105+
this.points[x].push(new Point(pathingStartX + x, pathingStartY + y, x, y));
102106
}
103107
}
104108

105109
// Center point
106-
this.openPoints.push(this.points[radius + 1][radius + 1]);
110+
this.openPoints.push(this.points[radius][radius]);
107111

108112
while(this.openPoints.length > 0) {
109113
this.currentPoint = this.calculateBestPoint();
@@ -115,18 +119,137 @@ export class Pathfinding {
115119
this.openPoints.splice(this.openPoints.indexOf(this.currentPoint), 1);
116120
this.closedPoints.push(this.currentPoint);
117121

118-
let x = this.currentPoint.x;
119-
let y = this.currentPoint.y;
120122
let level = this.actor.position.level;
121-
let currentPosition = new Position(x, y, level);
123+
let { x, y, indexX, indexY } = this.currentPoint;
124+
125+
// West
126+
if(indexX > 0 && this.canPathNSEW(new Position(x - 1, y, level), 0x1280108)) {
127+
this.calculateCost(this.points[indexX - 1][indexY]);
128+
}
129+
130+
// East
131+
if(indexX < pointLen - 1 && this.canPathNSEW(new Position(x + 1, y, level), 0x1280180)) {
132+
this.calculateCost(this.points[indexX + 1][indexY]);
133+
}
134+
135+
// South
136+
if(indexY > 0 && this.canPathNSEW(new Position(x, y - 1, level), 0x1280102)) {
137+
this.calculateCost(this.points[indexX][indexY - 1]);
138+
}
139+
140+
// North
141+
if(indexY < pointLen - 1 && this.canPathNSEW(new Position(x, y + 1, level), 0x1280120)) {
142+
this.calculateCost(this.points[indexX][indexY + 1]);
143+
}
144+
145+
// South-West
146+
if(indexX > 0 && indexY > 0) {
147+
if(this.canPathDiagonally(this.currentPoint.x, this.currentPoint.y, new Position(x - 1, y - 1, level), -1, -1,
148+
0x128010e, 0x1280108, 0x1280102)) {
149+
this.calculateCost(this.points[indexX - 1][indexY - 1]);
150+
}
151+
}
152+
153+
// South-East
154+
if(indexX < pointLen - 1 && indexY > 0) {
155+
if(this.canPathDiagonally(this.currentPoint.x, this.currentPoint.y, new Position(x + 1, y - 1, level), 1, -1,
156+
0x1280183, 0x1280180, 0x1280102)) {
157+
this.calculateCost(this.points[indexX + 1][indexY - 1]);
158+
}
159+
}
160+
161+
// North-West
162+
if(indexX > 0 && indexY < pointLen - 1) {
163+
if(this.canPathDiagonally(this.currentPoint.x, this.currentPoint.y, new Position(x - 1, y + 1, level), -1, 1,
164+
0x1280138, 0x1280108, 0x1280120)) {
165+
this.calculateCost(this.points[indexX - 1][indexY + 1]);
166+
}
167+
}
168+
169+
// North-East
170+
if(indexX < pointLen - 1 && indexY < pointLen - 1) {
171+
if(this.canPathDiagonally(this.currentPoint.x, this.currentPoint.y, new Position(x + 1, y + 1, level), 1, 1,
172+
0x12801e0, 0x1280180, 0x1280120)) {
173+
this.calculateCost(this.points[indexX + 1][indexY + 1]);
174+
}
175+
}
176+
}
177+
178+
const destinationPoint = this.points[destinationX - pathingStartX][destinationY - pathingStartY];
179+
180+
if(destinationPoint === null || destinationPoint.parent === null) {
181+
return null;
182+
}
183+
184+
// build path
185+
const path: Point[] = [];
186+
let point = destinationPoint;
187+
188+
while(!point.equals(this.points[radius][radius])) {
189+
path.push(point);
190+
point = point.parent;
191+
192+
if(point === null) {
193+
return null;
194+
}
195+
}
196+
197+
return path.reverse();
198+
}
199+
200+
private calculateCost(point: Point): void {
201+
const differenceX = this.currentPoint.x - point.x;
202+
const differenceY = this.currentPoint.y - point.y;
203+
const nextStepCost = this.currentPoint.cost + ((Math.abs(differenceX) + Math.abs(differenceY)) * 10);
204+
205+
if(nextStepCost < point.cost) {
206+
this.openPoints.splice(this.openPoints.indexOf(point));
207+
this.closedPoints.splice(this.closedPoints.indexOf(point));
208+
}
209+
210+
if(this.openPoints.indexOf(point) === -1 && this.closedPoints.indexOf(point) === -1) {
211+
point.parent = this.currentPoint;
212+
point.cost = nextStepCost;
213+
this.openPoints.push(point);
214+
}
215+
}
216+
217+
private calculateBestPoint(): Point {
218+
let bestPoint: Point = null;
219+
220+
for(const point of this.openPoints) {
221+
if(bestPoint === null) {
222+
bestPoint = point;
223+
continue;
224+
}
122225

123-
let testPosition = new Position(x - 1, y, level);
124-
if(this.canMoveTo(currentPosition, testPosition)) {
125-
const point = this.points[x - 1][y];
226+
if(point.cost < bestPoint.cost) {
227+
bestPoint = point;
126228
}
127229
}
230+
231+
return bestPoint;
128232
}
129233

234+
private canPathNSEW(position: Position, i: number): boolean {
235+
const chunk = world.chunkManager.getChunkForWorldPosition(position);
236+
const destinationAdjacency: number[][] = chunk.collisionMap.adjacency;
237+
const destinationLocalX: number = position.x - chunk.collisionMap.insetX;
238+
const destinationLocalY: number = position.y - chunk.collisionMap.insetY;
239+
return Pathfinding.canMoveNSEW(destinationAdjacency, destinationLocalX, destinationLocalY, i);
240+
}
241+
242+
private canPathDiagonally(originX: number, originY: number, position: Position, offsetX: number, offsetY: number,
243+
destMask: number, cornerMask1: number, cornerMask2: number): boolean {
244+
const chunk = world.chunkManager.getChunkForWorldPosition(position);
245+
const destinationAdjacency: number[][] = chunk.collisionMap.adjacency;
246+
const destinationLocalX: number = position.x - chunk.collisionMap.insetX;
247+
const destinationLocalY: number = position.y - chunk.collisionMap.insetY;
248+
return Pathfinding.canMoveDiagonally(position, destinationAdjacency, destinationLocalX, destinationLocalY,
249+
originX, originY, offsetX, offsetY, destMask, cornerMask1, cornerMask2);
250+
}
251+
252+
130253
public canMoveTo(origin: Position, destination: Position): boolean {
131254
const destinationChunk: Chunk = world.chunkManager.getChunkForWorldPosition(destination);
132255
const tile: Tile = destinationChunk.getTile(destination);
@@ -143,28 +266,28 @@ export class Pathfinding {
143266

144267
// West
145268
if(destination.x < initialX && destination.y == initialY) {
146-
if((destinationAdjacency[destinationLocalX][destinationLocalY] & 0x1280108) != 0) {
269+
if(!Pathfinding.canMoveNSEW(destinationAdjacency, destinationLocalX, destinationLocalY, 0x1280108)) {
147270
return false;
148271
}
149272
}
150273

151274
// East
152275
if(destination.x > initialX && destination.y == initialY) {
153-
if((destinationAdjacency[destinationLocalX][destinationLocalY] & 0x1280180) != 0) {
276+
if(!Pathfinding.canMoveNSEW(destinationAdjacency, destinationLocalX, destinationLocalY, 0x1280180)) {
154277
return false;
155278
}
156279
}
157280

158281
// South
159282
if(destination.y < initialY && destination.x == initialX) {
160-
if((destinationAdjacency[destinationLocalX][destinationLocalY] & 0x1280102) != 0) {
283+
if(!Pathfinding.canMoveNSEW(destinationAdjacency, destinationLocalX, destinationLocalY, 0x1280102)) {
161284
return false;
162285
}
163286
}
164287

165288
// North
166289
if(destination.y > initialY && destination.x == initialX) {
167-
if((destinationAdjacency[destinationLocalX][destinationLocalY] & 0x1280120) != 0) {
290+
if(!Pathfinding.canMoveNSEW(destinationAdjacency, destinationLocalX, destinationLocalY, 0x1280120)) {
168291
return false;
169292
}
170293
}
@@ -204,6 +327,10 @@ export class Pathfinding {
204327
return true;
205328
}
206329

330+
public static canMoveNSEW(destinationAdjacency: number[][], destinationLocalX: number, destinationLocalY: number, i: number): boolean {
331+
return (destinationAdjacency[destinationLocalX][destinationLocalY] & i) === 0;
332+
}
333+
207334
public static canMoveDiagonally(origin: Position, destinationAdjacency: number[][], destinationLocalX: number, destinationLocalY: number,
208335
initialX: number, initialY: number, offsetX: number, offsetY: number, destMask: number, cornerMask1: number, cornerMask2: number): boolean {
209336
const cornerX1: number = initialX + offsetX;
@@ -232,21 +359,4 @@ export class Pathfinding {
232359
return { localX, localY, chunk: cornerChunk };
233360
}
234361

235-
private calculateBestPoint(): Point {
236-
let bestPoint: Point = null;
237-
238-
for(const point of this.openPoints) {
239-
if(bestPoint === null) {
240-
bestPoint = point;
241-
continue;
242-
}
243-
244-
if(point.cost < bestPoint.cost) {
245-
bestPoint = point;
246-
}
247-
}
248-
249-
return bestPoint;
250-
}
251-
252362
}

src/world/actor/player/action/input-command-action.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Player } from '../player';
22
import { ActionPlugin } from '@server/plugins/plugin';
3+
import { logger } from '@runejs/logger/dist/logger';
34

45
/**
56
* The definition for a command action function.
@@ -115,6 +116,7 @@ export const inputCommandAction = (player: Player, command: string, inputArgs: s
115116
}
116117
} catch(commandError) {
117118
player.outgoingPackets.chatboxMessage(`Command error: ${commandError}`);
119+
logger.error(commandError);
118120
}
119121
});
120122
};

0 commit comments

Comments
 (0)