diff --git a/src/GridTilemap/CharBlockCache/CharBlockCache.ts b/src/GridTilemap/CharBlockCache/CharBlockCache.ts index 2cc938ab..9e7d8537 100644 --- a/src/GridTilemap/CharBlockCache/CharBlockCache.ts +++ b/src/GridTilemap/CharBlockCache/CharBlockCache.ts @@ -19,7 +19,7 @@ import { CollisionStrategy } from "../../Collisions/CollisionStrategy.js"; import { LayerVecPos } from "../../Utils/LayerPositionUtils/LayerPositionUtils.js"; export class CharBlockCache { - private tilePosToCharacters: Map> = new Map(); + private tilePosToCharacters: Cache = new Cache(); private charRemoved$ = new Subject(); constructor( @@ -35,8 +35,7 @@ export class CharBlockCache { ignoredCollisionGroups = new Set(), ): boolean { if (collisionGroups.length === 0) return false; - const posStr = this.posToString(pos, layer); - const charSet = this.tilePosToCharacters.get(posStr); + const charSet = this.tilePosToCharacters.get(pos, layer); return !!( charSet && charSet.size > 0 && @@ -72,9 +71,8 @@ export class CharBlockCache { } getCharactersAt(pos: Vector2, layer?: string): Set { - const posStr = this.posToString(pos, layer); - const characters = this.tilePosToCharacters.get(posStr); - return new Set(characters); + const characters = this.tilePosToCharacters.get(pos, layer); + return characters || new Set(); } addCharacter(character: GridCharacter): void { @@ -92,11 +90,12 @@ export class CharBlockCache { this.deleteTilePositions(character.getNextTilePos(), character); } - private add(pos: string, character: GridCharacter): void { - if (!this.tilePosToCharacters.has(pos)) { - this.tilePosToCharacters.set(pos, new Set()); + private add(pos: Vector2, layer: CharLayer, character: GridCharacter): void { + const set = this.tilePosToCharacters.get(pos, layer); + if (!set) { + this.tilePosToCharacters.set(pos, layer, new Set([character])); } - this.tilePosToCharacters.get(pos)?.add(character); + set?.add(character); } private addTilePosSetSub(character: GridCharacter) { @@ -148,7 +147,7 @@ export class CharBlockCache { private addTilePositions(pos: LayerVecPos, character: GridCharacter): void { this.forEachCharTile(pos, character, (x, y) => { - this.add(this.posToString(new Vector2(x, y), pos.layer), character); + this.add(new Vector2(x, y), pos.layer, character); }); } @@ -158,7 +157,7 @@ export class CharBlockCache { ): void { this.forEachCharTile(pos, character, (x, y) => { this.tilePosToCharacters - .get(this.posToString(new Vector2(x, y), pos.layer)) + .get(new Vector2(x, y), pos.layer) ?.delete(character); }); } @@ -203,3 +202,42 @@ export class CharBlockCache { return `${pos.x}#${pos.y}#${layer}`; } } + +class Cache { + private memo: Map< + /* parentX */ number, + Map< + /* parentY */ number, + Map> + > + > = new Map(); + + set(pos: Vector2, layer: CharLayer, val: Set) { + let pX = this.memo.get(pos.x); + if (!pX) { + pX = new Map(); + this.memo.set(pos.x, pX); + } + + let pY = pX.get(pos.y); + if (!pY) { + pY = new Map(); + pX.set(pos.y, pY); + } + + pY.set(layer, val); + } + + /** + * Returns null if no entry was found. undefined is a valid cached result. + */ + get(pos: Vector2, layer: CharLayer): Set | undefined { + const pX = this.memo.get(pos.x); + if (!pX) return undefined; + + const pY = pX.get(pos.y); + if (!pY) return undefined; + + return pY.get(layer); + } +} diff --git a/src/Pathfinding/Jps4/Jps4.ts b/src/Pathfinding/Jps4/Jps4.ts index 10f7fc64..76b64c52 100644 --- a/src/Pathfinding/Jps4/Jps4.ts +++ b/src/Pathfinding/Jps4/Jps4.ts @@ -7,21 +7,16 @@ import { Direction, directionFromPos, directionVector, - isHorizontal, - isVertical, - NumberOfDirections, - turnClockwise, } from "../../Direction/Direction.js"; import { Vector2 } from "../../Utils/Vector2/Vector2.js"; import { LayerPositionUtils, LayerVecPos, } from "../../Utils/LayerPositionUtils/LayerPositionUtils.js"; -import { DistanceUtilsFactory } from "../../Utils/DistanceUtilsFactory/DistanceUtilsFactory.js"; -import { DistanceUtils } from "../../Utils/DistanceUtils.js"; import { GridTilemap } from "../../GridTilemap/GridTilemap.js"; import { VectorUtils } from "../../Utils/VectorUtils.js"; import { PathfindingOptions } from "../PathfindingOptions.js"; +import { Position } from "../../Position.js"; interface ShortestPathTuple { previous: Map; @@ -40,30 +35,15 @@ export class Jps4 extends ShortestPathAlgorithm { }; private smallestDistToTarget = 0; - private steps = 0; + protected steps = 0; + protected visits: Position[] = []; private maxFrontierSize = 0; protected maxJumpSize = 0; - private turnOrder = { - [Direction.RIGHT]: 0, - [Direction.DOWN_RIGHT]: 1, - [Direction.DOWN]: 2, - [Direction.DOWN_LEFT]: 3, - [Direction.LEFT]: 4, - [Direction.UP_LEFT]: 5, - [Direction.UP]: 6, - [Direction.UP_RIGHT]: 7, - }; - - private turnTimes: Map> = createTurnTimes(); - - protected distanceUtils: DistanceUtils; - constructor(gridTilemap: GridTilemap, po: PathfindingOptions = {}) { super(gridTilemap, po); - this.distanceUtils = DistanceUtilsFactory.create(NumberOfDirections.FOUR); } findShortestPathImpl( @@ -166,12 +146,12 @@ export class Jps4 extends ShortestPathAlgorithm { } protected addIfNotBlocked( - set: Set, + arr: LayerVecPos[], src: LayerVecPos, target: LayerVecPos, ) { if (!this.blockOrTrans(src, target)) { - set.add(target); + arr.push(target); } } @@ -182,7 +162,7 @@ export class Jps4 extends ShortestPathAlgorithm { ); } - private getNeighborsInternal( + protected getNeighborsInternal( node: LayerVecPos, parent: LayerVecPos | undefined, stopNode: LayerVecPos, @@ -194,42 +174,64 @@ export class Jps4 extends ShortestPathAlgorithm { })); } - const pruned = this.prune(parent, node).map((unblockedNeighbor) => { - const transition = this.getTransition( - unblockedNeighbor.position, - node.layer, - ); - return { - position: unblockedNeighbor.position, - layer: transition || node.layer, - }; - }); + const pruned = this.prune(parent, node) + .filter( + (neighbor) => !this.isBlockingIgnoreTarget(node, neighbor, stopNode), + ) + .map((unblockedNeighbor) => { + const transition = this.getTransition( + unblockedNeighbor.position, + node.layer, + ); + return { + position: unblockedNeighbor.position, + layer: transition || node.layer, + }; + }); const successors: { p: LayerVecPos; dist: number }[] = []; for (const p of pruned) { - const j = this.jump(node, p, stopNode, 1); - if (j) { - successors.push(j); + if (this.isHorizontal(node.position, p.position)) { + successors.push({ p, dist: 1 }); + } else { + const j = this.jump( + node, + p, + stopNode, + 1, + directionFromPos(node.position, p.position), + ); + if (j) { + successors.push(j); + } } } return successors; } + private isBlockingIgnoreTarget( + src: LayerVecPos, + target: LayerVecPos, + stopNode: LayerVecPos, + ): boolean { + return ( + this.isBlocking(src, target) && + !( + this.options.ignoreBlockedTarget && + LayerPositionUtils.equal(target, stopNode) + ) + ); + } + protected jump( parent: LayerVecPos, node: LayerVecPos, stopNode: LayerVecPos, dist: number, + dir: Direction, ): { p: LayerVecPos; dist: number } | undefined { - const dir = this.distanceUtils.direction(parent.position, node.position); - if ( - this.isBlocking(parent, node) && - !( - LayerPositionUtils.equal(node, stopNode) && - this.options.ignoreBlockedTarget - ) - ) { + if (this.isBlockingIgnoreTarget(parent, node, stopNode)) { return undefined; } if (LayerPositionUtils.equal(node, stopNode)) { @@ -241,7 +243,6 @@ export class Jps4 extends ShortestPathAlgorithm { if (this.getTransition(node.position, parent.layer) !== undefined) { return { p: node, dist }; } - if (isHorizontal(dir)) return { p: node, dist }; if (this.hasForced(parent, node)) { return { p: node, dist }; } @@ -249,78 +250,62 @@ export class Jps4 extends ShortestPathAlgorithm { this.updateClosestToTarget(node, stopNode); return this.jump( node, - this.getTilePosInDir( - node, - directionFromPos(parent.position, node.position), - ), + this.getTilePosInDir(node, dir), stopNode, dist + 1, + dir, ); } + private isHorizontal(p1: Position, p2: Position): boolean { + return p1.y === p2.y; + } + protected getForced( parent: LayerVecPos, node: LayerVecPos, - ): Set { - const res = new Set(); + downLeft: LayerVecPos, + bottom: LayerVecPos, + topLeft: LayerVecPos, + top: LayerVecPos, + ): LayerVecPos[] { + const res: LayerVecPos[] = []; - // if parent is more than one step away (jump), take the closest one: - const newParent = this.posInDir( - node, - this.distanceUtils.direction(node.position, parent.position), - ); - const { topLeft, downLeft, top, bottom } = this.normalizedPositions( - newParent, - node, - ); + const newParent = parent; - const dir = this.distanceUtils.direction(parent.position, node.position); - if (isVertical(dir)) { - if ( - this.blockOrTrans(newParent, downLeft) || - this.blockOrTrans(downLeft, bottom) - ) { - this.addIfNotBlocked(res, node, bottom); - } - if ( - this.blockOrTrans(newParent, topLeft) || - this.blockOrTrans(topLeft, top) - ) { - this.addIfNotBlocked(res, node, top); - } + if ( + this.blockOrTrans(newParent, downLeft) || + this.blockOrTrans(downLeft, bottom) + ) { + this.addIfNotBlocked(res, node, bottom); + } + if ( + this.blockOrTrans(newParent, topLeft) || + this.blockOrTrans(topLeft, top) + ) { + this.addIfNotBlocked(res, node, top); } return res; } protected hasForced(parent: LayerVecPos, node: LayerVecPos): boolean { - // if parent is more than one step away (jump), take the closest one: - const newParent = this.posInDir( - node, - this.distanceUtils.direction(node.position, parent.position), - ); const { topLeft, downLeft, top, bottom } = this.normalizedPositions( - newParent, + parent, node, ); - const dir = this.distanceUtils.direction(parent.position, node.position); - if (isVertical(dir)) { - if ( - this.blockOrTrans(newParent, downLeft) || - this.blockOrTrans(downLeft, bottom) - ) { - if (!this.blockOrTrans(node, bottom)) { - return true; - } + if ( + this.blockOrTrans(parent, downLeft) || + this.blockOrTrans(downLeft, bottom) + ) { + if (!this.blockOrTrans(node, bottom)) { + return true; } - if ( - this.blockOrTrans(newParent, topLeft) || - this.blockOrTrans(topLeft, top) - ) { - if (!this.blockOrTrans(node, top)) { - return true; - } + } + if (this.blockOrTrans(parent, topLeft) || this.blockOrTrans(topLeft, top)) { + if (!this.blockOrTrans(node, top)) { + return true; } } @@ -328,14 +313,17 @@ export class Jps4 extends ShortestPathAlgorithm { } protected prune(parent: LayerVecPos, node: LayerVecPos): LayerVecPos[] { - const { right, top, bottom } = this.normalizedPositions(parent, node); - const forced = this.getForced(parent, node); - const dir = directionFromPos(parent.position, node.position); - - if (isHorizontal(dir)) { + const { right, top, bottom, downLeft, topLeft } = this.normalizedPositions( + parent, + node, + ); + if (this.isHorizontal(parent.position, node.position)) { return [right, top, bottom]; } - return [right, ...forced]; + return [ + right, + ...this.getForced(parent, node, downLeft, bottom, topLeft, top), + ]; } protected normalizedPositions( @@ -344,51 +332,109 @@ export class Jps4 extends ShortestPathAlgorithm { ): { topLeft: LayerVecPos; downLeft: LayerVecPos; - downRight: LayerVecPos; - topRight: LayerVecPos; top: LayerVecPos; bottom: LayerVecPos; right: LayerVecPos; } { - const dir = directionFromPos(parent.position, node.position); - - return { - topLeft: this.posInDir( - node, - this.turnTimes.get(Direction.UP_LEFT)?.get(this.turnOrder[dir]) || - Direction.UP_LEFT, - ), - downLeft: this.posInDir( - node, - this.turnTimes.get(Direction.DOWN_LEFT)?.get(this.turnOrder[dir]) || - Direction.DOWN_LEFT, - ), - downRight: this.posInDir( - node, - this.turnTimes.get(Direction.DOWN_RIGHT)?.get(this.turnOrder[dir]) || - Direction.DOWN_RIGHT, - ), - topRight: this.posInDir( - node, - this.turnTimes.get(Direction.UP_RIGHT)?.get(this.turnOrder[dir]) || - Direction.UP_RIGHT, - ), - top: this.posInDir( - node, - this.turnTimes.get(Direction.UP)?.get(this.turnOrder[dir]) || - Direction.UP, - ), - bottom: this.posInDir( - node, - this.turnTimes.get(Direction.DOWN)?.get(this.turnOrder[dir]) || - Direction.DOWN, - ), - right: this.posInDir( - node, - this.turnTimes.get(Direction.RIGHT)?.get(this.turnOrder[dir]) || - Direction.RIGHT, - ), - }; + // case 1 p->n: + if (parent.position.x < node.position.x) { + return { + topLeft: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + }; + // case 2 n<-p: + } else if (parent.position.x > node.position.x) { + return { + topLeft: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + }; + // case 3 p + // n + } else if (parent.position.y < node.position.y) { + return { + topLeft: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + }; + // case 4 n + // p + } else { + return { + topLeft: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + }; + } } protected posInDir(pos: LayerVecPos, dir: Direction): LayerVecPos { @@ -434,30 +480,3 @@ export class Jps4 extends ShortestPathAlgorithm { function safeGet(map: Map, position: LayerVecPos): number { return map.get(LayerPositionUtils.toString(position)) ?? Number.MAX_VALUE; } - -function createTurnTimes(): Map> { - const dirs = [ - Direction.RIGHT, - Direction.DOWN_RIGHT, - Direction.DOWN, - Direction.DOWN_LEFT, - Direction.LEFT, - Direction.UP_LEFT, - Direction.UP, - Direction.UP_RIGHT, - ]; - const dirMap: Map> = new Map(); - - for (let i = 0; i < dirs.length; i++) { - const subMap: Map = new Map(); - let currentDir = dirs[i]; - subMap.set(0, currentDir); - for (let j = 1; j <= 7; j++) { - currentDir = turnClockwise(currentDir); - subMap.set(j, currentDir); - } - dirMap.set(dirs[i], subMap); - } - - return dirMap; -} diff --git a/src/Pathfinding/Jps8/Jps8.ts b/src/Pathfinding/Jps8/Jps8.ts index e98889ab..1db356c9 100644 --- a/src/Pathfinding/Jps8/Jps8.ts +++ b/src/Pathfinding/Jps8/Jps8.ts @@ -1,38 +1,81 @@ import { directionFromPos, isDiagonal } from "../../Direction/Direction.js"; -import { Direction, NumberOfDirections } from "../../GridEngineHeadless.js"; +import { Direction } from "../../GridEngineHeadless.js"; import { GridTilemap } from "../../GridTilemap/GridTilemap.js"; -import { DistanceUtilsFactory } from "../../Utils/DistanceUtilsFactory/DistanceUtilsFactory.js"; +import { CharLayer } from "../../Position.js"; import { LayerPositionUtils, LayerVecPos, } from "../../Utils/LayerPositionUtils/LayerPositionUtils.js"; +import { Vector2 } from "../../Utils/Vector2/Vector2.js"; import { Jps4 } from "../Jps4/Jps4.js"; import { PathfindingOptions } from "../PathfindingOptions.js"; +import { ShortestPathResult } from "../ShortestPathAlgorithm.js"; export class Jps8 extends Jps4 { + private jumpCache: JumpCache = new JumpCache(); constructor(gridTilemap: GridTilemap, po: PathfindingOptions = {}) { super(gridTilemap, po); - this.distanceUtils = DistanceUtilsFactory.create(NumberOfDirections.EIGHT); } - protected getForced( - parent: LayerVecPos, + findShortestPathImpl( + startPos: LayerVecPos, + targetPos: LayerVecPos, + ): ShortestPathResult { + this.jumpCache = new JumpCache(); + return super.findShortestPathImpl(startPos, targetPos); + } + + protected getNeighborsInternal( node: LayerVecPos, - ): Set { - const res = new Set(); + parent: LayerVecPos | undefined, + stopNode: LayerVecPos, + ): { p: LayerVecPos; dist: number }[] { + if (!parent || node.layer !== parent.layer) { + return this.getNeighbors(node, stopNode).map((n) => ({ + p: n, + dist: 1, + })); + } + + const pruned = this.prune(parent, node).map((unblockedNeighbor) => { + const transition = this.getTransition( + unblockedNeighbor.position, + node.layer, + ); + return { + position: unblockedNeighbor.position, + layer: transition || node.layer, + }; + }); + + const successors: { p: LayerVecPos; dist: number }[] = []; + for (const p of pruned) { + const j = this.jump( + node, + p, + stopNode, + 1, + directionFromPos(node.position, p.position), + ); + if (j) { + j.dist = this.distance(node.position, j.p.position); + successors.push(j); + } + } + + return successors; + } + + protected getForced(parent: LayerVecPos, node: LayerVecPos): LayerVecPos[] { + const res: LayerVecPos[] = []; - // if parent is more than one step away (jump), take the closest one: - const newParent = this.posInDir( - node, - this.distanceUtils.direction(node.position, parent.position), - ); const { topLeft, downLeft, top, bottom, topRight, downRight } = - this.normalizedPositions(newParent, node); + this.normalizedPositions(parent, node); - const dir = this.distanceUtils.direction(parent.position, node.position); + const dir = directionFromPos(parent.position, node.position); if (isDiagonal(dir)) { - if (this.blockOrTrans(newParent, topLeft)) { + if (this.blockOrTrans(parent, topLeft)) { this.addIfNotBlocked(res, node, top); this.addIfNotBlocked(res, node, topRight); @@ -41,7 +84,7 @@ export class Jps8 extends Jps4 { } } - if (this.blockOrTrans(newParent, downLeft)) { + if (this.blockOrTrans(parent, downLeft)) { this.addIfNotBlocked(res, node, bottom); this.addIfNotBlocked(res, node, downRight); @@ -66,43 +109,37 @@ export class Jps8 extends Jps4 { this.addIfNotBlocked(res, node, downRight); } } else { - if ( - this.blockOrTrans(newParent, top) || - this.blockOrTrans(top, topRight) - ) { + if (this.blockOrTrans(parent, top) || this.blockOrTrans(top, topRight)) { this.addIfNotBlocked(res, node, topRight); } if ( - this.blockOrTrans(newParent, bottom) || + this.blockOrTrans(parent, bottom) || this.blockOrTrans(bottom, downRight) ) { this.addIfNotBlocked(res, node, downRight); } if ( - this.blockOrTrans(newParent, topLeft) && - this.blockOrTrans(newParent, top) + this.blockOrTrans(parent, topLeft) && + this.blockOrTrans(parent, top) ) { this.addIfNotBlocked(res, node, top); this.addIfNotBlocked(res, node, topLeft); } if ( - this.blockOrTrans(newParent, downLeft) && - this.blockOrTrans(newParent, bottom) + this.blockOrTrans(parent, downLeft) && + this.blockOrTrans(parent, bottom) ) { this.addIfNotBlocked(res, node, bottom); this.addIfNotBlocked(res, node, downLeft); } - if ( - this.blockOrTrans(topLeft, top) && - this.blockOrTrans(newParent, top) - ) { + if (this.blockOrTrans(topLeft, top) && this.blockOrTrans(parent, top)) { this.addIfNotBlocked(res, node, top); } if ( this.blockOrTrans(downLeft, bottom) && - this.blockOrTrans(newParent, bottom) + this.blockOrTrans(parent, bottom) ) { this.addIfNotBlocked(res, node, bottom); } @@ -111,20 +148,14 @@ export class Jps8 extends Jps4 { return res; } - protected hasForced(parent: LayerVecPos, node: LayerVecPos): boolean { - // if parent is more than one step away (jump), take the closest one: - - const newParent = this.posInDir( - node, - this.distanceUtils.direction(node.position, parent.position), - ); + protected hasForced(parent: LayerVecPos, node: LayerVecPos) { const { topLeft, downLeft, top, bottom, topRight, downRight } = - this.normalizedPositions(newParent, node); + this.normalizedPositions(parent, node); - const dir = this.distanceUtils.direction(parent.position, node.position); + const dir = directionFromPos(parent.position, node.position); if (isDiagonal(dir)) { - if (this.blockOrTrans(newParent, topLeft)) { + if (this.blockOrTrans(parent, topLeft)) { if ( !this.blockOrTrans(node, top) || !this.blockOrTrans(node, topRight) @@ -139,7 +170,7 @@ export class Jps8 extends Jps4 { } } - if (this.blockOrTrans(newParent, downLeft)) { + if (this.blockOrTrans(parent, downLeft)) { if ( !this.blockOrTrans(node, bottom) || !this.blockOrTrans(node, downRight) @@ -178,16 +209,13 @@ export class Jps8 extends Jps4 { } } } else { - if ( - this.blockOrTrans(newParent, top) || - this.blockOrTrans(top, topRight) - ) { + if (this.blockOrTrans(parent, top) || this.blockOrTrans(top, topRight)) { if (!this.blockOrTrans(node, topRight)) { return true; } } if ( - this.blockOrTrans(newParent, bottom) || + this.blockOrTrans(parent, bottom) || this.blockOrTrans(bottom, downRight) ) { if (!this.blockOrTrans(node, downRight)) { @@ -196,8 +224,8 @@ export class Jps8 extends Jps4 { } if ( - this.blockOrTrans(newParent, topLeft) && - this.blockOrTrans(newParent, top) + this.blockOrTrans(parent, topLeft) && + this.blockOrTrans(parent, top) ) { if ( !this.blockOrTrans(node, top) || @@ -207,8 +235,8 @@ export class Jps8 extends Jps4 { } } if ( - this.blockOrTrans(newParent, downLeft) && - this.blockOrTrans(newParent, bottom) + this.blockOrTrans(parent, downLeft) && + this.blockOrTrans(parent, bottom) ) { if ( !this.blockOrTrans(node, bottom) || @@ -218,17 +246,14 @@ export class Jps8 extends Jps4 { } } - if ( - this.blockOrTrans(topLeft, top) && - this.blockOrTrans(newParent, top) - ) { + if (this.blockOrTrans(topLeft, top) && this.blockOrTrans(parent, top)) { if (!this.blockOrTrans(node, top)) { return true; } } if ( this.blockOrTrans(downLeft, bottom) && - this.blockOrTrans(newParent, bottom) + this.blockOrTrans(parent, bottom) ) { if (!this.blockOrTrans(node, bottom)) { return true; @@ -256,32 +281,35 @@ export class Jps8 extends Jps4 { node: LayerVecPos, stopNode: LayerVecPos, dist: number, + dir: Direction, ): { p: LayerVecPos; dist: number } | undefined { - const dir = this.distanceUtils.direction(parent.position, node.position); - const parentDirPos = this.getTilePosInDir( - node, - this.distanceUtils.direction(node.position, parent.position), - ); + const memo = this.jumpCache.get(parent, node); + if (memo !== null) return memo; if ( - this.isBlocking(parentDirPos, node) && + this.isBlocking(parent, node) && !( LayerPositionUtils.equal(node, stopNode) && this.options.ignoreBlockedTarget ) ) { + this.jumpCache.set(parent, node, undefined); return undefined; } if (LayerPositionUtils.equal(node, stopNode)) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } if (dist >= this.maxJumpSize) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } if (this.getTransition(node.position, parent.layer) !== undefined) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } if (this.hasForced(parent, node)) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } this.updateClosestToTarget(node, stopNode); @@ -292,9 +320,11 @@ export class Jps8 extends Jps4 { this.getTilePosInDir(node, Direction.UP), stopNode, dist + 1, + Direction.UP, ) !== undefined ) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } if ( this.jump( @@ -302,9 +332,11 @@ export class Jps8 extends Jps4 { this.getTilePosInDir(node, Direction.LEFT), stopNode, dist + 1, + Direction.LEFT, ) !== undefined ) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } } else if (dir === Direction.DOWN_LEFT) { if ( @@ -313,9 +345,11 @@ export class Jps8 extends Jps4 { this.getTilePosInDir(node, Direction.DOWN), stopNode, dist + 1, + Direction.DOWN, ) !== undefined ) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } if ( this.jump( @@ -323,9 +357,11 @@ export class Jps8 extends Jps4 { this.getTilePosInDir(node, Direction.LEFT), stopNode, dist + 1, + Direction.LEFT, ) !== undefined ) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } } else if (dir === Direction.UP_RIGHT) { if ( @@ -334,9 +370,11 @@ export class Jps8 extends Jps4 { this.getTilePosInDir(node, Direction.UP), stopNode, dist + 1, + Direction.UP, ) !== undefined ) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } if ( this.jump( @@ -344,9 +382,11 @@ export class Jps8 extends Jps4 { this.getTilePosInDir(node, Direction.RIGHT), stopNode, dist + 1, + Direction.RIGHT, ) !== undefined ) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } } else if (dir === Direction.DOWN_RIGHT) { if ( @@ -355,9 +395,11 @@ export class Jps8 extends Jps4 { this.getTilePosInDir(node, Direction.DOWN), stopNode, dist + 1, + Direction.DOWN, ) !== undefined ) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } if ( this.jump( @@ -365,20 +407,419 @@ export class Jps8 extends Jps4 { this.getTilePosInDir(node, Direction.RIGHT), stopNode, dist + 1, + Direction.RIGHT, ) !== undefined ) { - return { p: node, dist }; + this.jumpCache.set(parent, node, { p: node, dist: 0 }); + return { p: node, dist: 0 }; } } - return this.jump( + const res = this.jump( node, - this.getTilePosInDir( - node, - directionFromPos(parent.position, node.position), - ), + this.getTilePosInDir(node, dir), stopNode, dist + 1, + dir, ); + this.jumpCache.set(parent, node, res); + return res; + } + + protected normalizedPositions( + parent: LayerVecPos, + node: LayerVecPos, + ): { + topLeft: LayerVecPos; + downLeft: LayerVecPos; + top: LayerVecPos; + bottom: LayerVecPos; + right: LayerVecPos; + downRight: LayerVecPos; + topRight: LayerVecPos; + } { + // case 1 p->n: + if ( + parent.position.x < node.position.x && + parent.position.y === node.position.y + ) { + return { + topLeft: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + topRight: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + downRight: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + }; + // case 2 n<-p: + } else if ( + parent.position.x > node.position.x && + parent.position.y === node.position.y + ) { + return { + topLeft: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + topRight: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + downRight: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + }; + // case 3 p + // n + } else if ( + parent.position.y < node.position.y && + parent.position.x === node.position.x + ) { + return { + topLeft: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + topRight: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + downRight: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + }; + // case 4 n + // p + } else if ( + parent.position.y > node.position.y && + parent.position.x === node.position.x + ) { + return { + topLeft: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + topRight: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + downRight: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + }; + // case 5 p + // .n + } else if ( + parent.position.y < node.position.y && + parent.position.x < node.position.x + ) { + return { + topLeft: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + topRight: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + downRight: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + }; + // case 6 ..p + // .n + } else if ( + parent.position.y < node.position.y && + parent.position.x > node.position.x + ) { + return { + topLeft: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + topRight: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + downRight: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + }; + // case 7 .n. + // p + } else if ( + parent.position.y > node.position.y && + parent.position.x < node.position.x + ) { + return { + topLeft: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x + 1, node.position.y + 1), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + topRight: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + downRight: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + }; + // case 8 .n. + // ..p + } else { + return { + topLeft: { + position: new Vector2(node.position.x, node.position.y + 1), + layer: node.layer, + }, + downLeft: { + position: new Vector2(node.position.x + 1, node.position.y), + layer: node.layer, + }, + top: { + position: new Vector2(node.position.x - 1, node.position.y + 1), + layer: node.layer, + }, + bottom: { + position: new Vector2(node.position.x + 1, node.position.y - 1), + layer: node.layer, + }, + right: { + position: new Vector2(node.position.x - 1, node.position.y - 1), + layer: node.layer, + }, + topRight: { + position: new Vector2(node.position.x - 1, node.position.y), + layer: node.layer, + }, + downRight: { + position: new Vector2(node.position.x, node.position.y - 1), + layer: node.layer, + }, + }; + } + } +} + +class JumpCache { + private memo: Map< + /* parentX */ number, + Map< + /* parentY */ number, + Map< + /* parentLayer */ CharLayer, + Map< + /* nodeX */ number, + Map< + /*nodeY*/ number, + Map< + /* nodeLayer */ CharLayer, + { p: LayerVecPos; dist: number } | null + > + > + > + > + > + > = new Map(); + + set( + parent: LayerVecPos, + node: LayerVecPos, + val: { p: LayerVecPos; dist: number } | undefined, + ) { + let pX = this.memo.get(parent.position.x); + if (!pX) { + pX = new Map(); + this.memo.set(parent.position.x, pX); + } + + let pY = pX.get(parent.position.y); + if (!pY) { + pY = new Map(); + pX.set(parent.position.y, pY); + } + + let pL = pY.get(parent.layer); + if (!pL) { + pL = new Map(); + pY.set(parent.layer, pL); + } + + let nX = pL.get(node.position.x); + if (!nX) { + nX = new Map(); + pL.set(node.position.x, nX); + } + + let nY = nX.get(node.position.y); + if (!nY) { + nY = new Map(); + nX.set(node.position.y, nY); + } + + const nL = nY.get(node.layer); + if (!nL) { + if (val === undefined) { + nY.set(node.layer, null); + } else { + nY.set(node.layer, val); + } + } + } + + /** + * Returns null if no entry was found. undefined is a valid cached result. + */ + get( + parent: LayerVecPos, + node: LayerVecPos, + ): { p: LayerVecPos; dist: number } | undefined | null { + const pX = this.memo.get(parent.position.x); + if (!pX) return null; + + const pY = pX.get(parent.position.y); + if (!pY) return null; + + const pL = pY.get(parent.layer); + if (!pL) return null; + + const nX = pL.get(node.position.x); + if (!nX) return null; + + const nY = nX.get(node.position.y); + if (!nY) return null; + + const nL = nY.get(node.layer); + if (nL === undefined) { + return null; + } else if (nL === null) { + return undefined; + } + + return nL; } } diff --git a/src/Pathfinding/ShortestPathAlgorithm.ts b/src/Pathfinding/ShortestPathAlgorithm.ts index f5b3e0f5..224a50ee 100644 --- a/src/Pathfinding/ShortestPathAlgorithm.ts +++ b/src/Pathfinding/ShortestPathAlgorithm.ts @@ -4,6 +4,7 @@ import { Vector2 } from "../Utils/Vector2/Vector2.js"; import { Direction, directionFromPos, + directionVector, NumberOfDirections, } from "../Direction/Direction.js"; import { Concrete } from "../Utils/TypeUtils.js"; @@ -47,6 +48,8 @@ export interface ShortestPathResult { export abstract class ShortestPathAlgorithm { protected options: Concrete; + private ignoredCharsSet: Set; + findShortestPath( startPos: LayerVecPos, targetPos: LayerVecPos, @@ -101,6 +104,7 @@ export abstract class ShortestPathAlgorithm { considerCosts, calculateClosestToTarget, }; + this.ignoredCharsSet = new Set(ignoredChars); } getNeighbors(pos: LayerVecPos, dest: LayerVecPos): LayerVecPos[] { @@ -178,7 +182,7 @@ export abstract class ShortestPathAlgorithm { this.options.pathWidth, this.options.pathHeight, this.options.collisionGroups, - this.options.ignoredChars, + this.ignoredCharsSet, this.gridTilemap, ); @@ -194,6 +198,14 @@ export abstract class ShortestPathAlgorithm { } getTilePosInDir(pos: LayerVecPos, dir: Direction): LayerVecPos { + if (this.options.ignoreLayers) { + return { + position: pos.position.add( + directionVector(this.gridTilemap.toMapDirection(dir)), + ), + layer: pos.layer, + }; + } return this.gridTilemap.getTilePosInDirection(pos, dir); } @@ -246,20 +258,26 @@ export abstract class ShortestPathAlgorithm { pathWidth: number, pathHeight: number, collisionGroups: string[], - ignoredChars: CharId[], + ignoredChars: Set, gridTilemap: GridTilemap, ): boolean { + if (pathWidth === 1 && pathHeight === 1) { + return gridTilemap.hasBlockingChar( + dest.position, + dest.layer, + collisionGroups, + ignoredChars, + ); + } + const isBlocking = (pos: Vector2) => { return gridTilemap.hasBlockingChar( pos, dest.layer, collisionGroups, - new Set(ignoredChars), + ignoredChars, ); }; - if (pathWidth === 1 && pathHeight === 1) { - return isBlocking(dest.position); - } const dir = directionFromPos(src.position, dest.position); return this.isBlockingMultiTile(