mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-04 07:56:14 +00:00
Pathfinding Refactor pt. 2 (#2866)
## Playtest https://pf-pt-2.openfront.dev/ ## Pathfinding Refactor pt. 2 <img width="1536" height="1024" alt="image" src="https://github.com/user-attachments/assets/9477958e-54b7-4c83-b317-ba789e809e9e" /> This is a follow-up to a previous PR introducing pathfinding changes. This time, it introduces a complete refactor of `pathfinding` directory and breakdown into composable pieces. ### Unified PathFinder interface `PathFinder<T>` and `SteppingPathFinder<T>` are introduced to unify **all** pathfinding across the application. First one exposes complete path, while stepping variant allows the callee to iterate over the path by calling `.next`. All pathfinders share this one common interface, which makes them easy to use in any scenario - `PathFinding.Water(game).search(from, to)`. `SteppingPathFinder<T>` extends `PathFinder<T>` with an ability to iterate over the path. It handles caching, storing current index and invalidation. This allows the units to not care about the inner workings of the pathfinder and just call `pf.next(current, target)` and receive instructions on what to do next. ### Common entry point All pathfinders are now exposed from common `PathFinding` entrypoint: - `PathFinding.Water` - `PathFinding.Rail` - `PathFinding.Stations` - `PathFinding.Rail` Additional entry point is introduced for pathfinders which need to work both in the worker, but also on the frontend, which lacks `Game` interface. Currently only `UniversalPathFinding.Parabola` is available. ### Spatial Query New module has been introduced close to `pathfinding` - `SpatialQuery`. It aims to resolve any questions game may have about finding tiles meeting criteria. Currently `SpatialQuery.closestShore(player, target)` and `SpatialQuery.closestShoreByWater(player, target)` are available - they help answering questions about naval invasion: "What is the best landing location from user's click?" and "Which our tile should be used to launch the transport ship?". Under the hood they use very similar mechanics to pathfinding, so it felt right to put them close by. ### Modular architecture Pathfinders now support transformers: `MiniMapTransformer`, `ShoreCoercingTransformer`, `ComponentCheckTransformer`, `SmoothingTransformer`. Transformers functions like a middleware in the pathfinding chain. They wrap around the pathfinder and provide additional functionality. This allows the pathfinder to focus on actually finding the path instead of doing unrelated things. Example chain for simple (A*) water pathfinding: ```ts static WaterSimple(game: Game): SteppingPathFinder<TileRef> { const miniMap = game.miniMap(); const pf = new AStarWater(miniMap); return PathFinderBuilder.create(pf) .wrap((pf) => new ShoreCoercingTransformer(pf, miniMap)) .wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap)) .buildWithStepper(tileStepperConfig(game)); } ``` The Pathfinder - here `AStarWater` - does not care about the conversion between minimap and main map tiles. It also does not care if the source or destination is a land tile. The transformers take care of that. The pathfinder gets a set of valid coordinates and produces the path - that's it. Modular approach makes working on a particular set of utilities much easier - for example map upscaling is handled consistently across all pathfinders. Additionally, the pathfinders are not tied to the particular map resolution used. Pass them a different map and they will work the same. ### Algorithms Algorithms used are neatly organized inside `src/core/pathfinding/algorithms`. They are prefixed with the algorithm name and suffixed with the use case. File without suffix exposes generic version ready to traverse any graph with adapters. Specialized versions either use an adapter or inline logic when performance is critical - using adapters leads to 20-30% performance loss. The directory includes `A*` and `BFS` but also other useful utils, such as `AbstractGraph` used to generate... an abstract graph on top of the tile map and `ConnectedComponents` helping to identify whether two tiles are connected by a path without actually computing the path. ### Playground The playground have been updated with new algorithms, including tweaked very greedy `A*`. <img width="2175" height="1424" alt="image" src="https://github.com/user-attachments/assets/1f833651-0024-4299-bf86-882f5368358c" /> ### Tests Yeah, there are some, a little too many if I say so myself. But there are no useless tests. I had to ensure refactored code works somehow reliably. This PR comes with trust me bro guarantee, but I would appreciate someone confirming **naval invasions, nukes (esp. MIRV) and warships**. ### Discord `moleole` GL & HF
This commit is contained in:
committed by
GitHub
parent
bcec4ad758
commit
0e3ced3bfa
@@ -8,7 +8,9 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { ParabolaPathFinder } from "../pathfinding/PathFinding";
|
||||
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
||||
import { ParabolaUniversalPathFinder } from "../pathfinding/PathFinder.Parabola";
|
||||
import { PathStatus } from "../pathfinding/types";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { simpleHash } from "../Util";
|
||||
import { NukeExecution } from "./NukeExecution";
|
||||
@@ -30,11 +32,12 @@ export class MirvExecution implements Execution {
|
||||
|
||||
private random: PseudoRandom;
|
||||
|
||||
private pathFinder: ParabolaPathFinder;
|
||||
private pathFinder: ParabolaUniversalPathFinder;
|
||||
|
||||
private targetPlayer: Player | TerraNullius;
|
||||
|
||||
private separateDst: TileRef;
|
||||
private spawnTile: TileRef;
|
||||
|
||||
private speed: number = -1;
|
||||
|
||||
@@ -46,9 +49,11 @@ export class MirvExecution implements Execution {
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.random = new PseudoRandom(mg.ticks() + simpleHash(this.player.id()));
|
||||
this.mg = mg;
|
||||
this.pathFinder = new ParabolaPathFinder(mg);
|
||||
this.targetPlayer = this.mg.owner(this.dst);
|
||||
this.speed = this.mg.config().defaultNukeSpeed();
|
||||
this.pathFinder = UniversalPathFinding.Parabola(mg, {
|
||||
increment: this.speed,
|
||||
});
|
||||
|
||||
// Betrayal on launch
|
||||
if (this.targetPlayer.isPlayer()) {
|
||||
@@ -70,6 +75,7 @@ export class MirvExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.spawnTile = spawn;
|
||||
this.nuke = this.player.buildUnit(UnitType.MIRV, spawn, {
|
||||
targetTile: this.dst,
|
||||
});
|
||||
@@ -79,7 +85,6 @@ export class MirvExecution implements Execution {
|
||||
);
|
||||
const y = Math.max(0, this.mg.y(this.dst) - 500) + 50;
|
||||
this.separateDst = this.mg.ref(x, y);
|
||||
this.pathFinder.computeControlPoints(spawn, this.separateDst);
|
||||
|
||||
this.mg.displayIncomingUnit(
|
||||
this.nuke.id(),
|
||||
@@ -90,15 +95,19 @@ export class MirvExecution implements Execution {
|
||||
);
|
||||
}
|
||||
|
||||
const result = this.pathFinder.nextTile(this.speed);
|
||||
if (result === true) {
|
||||
const result = this.pathFinder.next(
|
||||
this.spawnTile,
|
||||
this.separateDst,
|
||||
this.speed,
|
||||
);
|
||||
if (result.status === PathStatus.COMPLETE) {
|
||||
this.separate();
|
||||
this.active = false;
|
||||
// Record stats
|
||||
this.mg.stats().bombLand(this.player, this.targetPlayer, UnitType.MIRV);
|
||||
return;
|
||||
} else {
|
||||
this.nuke.move(result);
|
||||
} else if (result.status === PathStatus.NEXT) {
|
||||
this.nuke.move(result.node);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { ParabolaPathFinder } from "../pathfinding/PathFinding";
|
||||
import { UniversalPathFinding } from "../pathfinding/PathFinder";
|
||||
import { ParabolaUniversalPathFinder } from "../pathfinding/PathFinder.Parabola";
|
||||
import { PathStatus } from "../pathfinding/types";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { NukeType } from "../StatsSchemas";
|
||||
import { computeNukeBlastCounts } from "./Util";
|
||||
@@ -22,7 +24,7 @@ export class NukeExecution implements Execution {
|
||||
private mg: Game;
|
||||
private nuke: Unit | null = null;
|
||||
private tilesToDestroyCache: Set<TileRef> | undefined;
|
||||
private pathFinder: ParabolaPathFinder;
|
||||
private pathFinder: ParabolaUniversalPathFinder;
|
||||
|
||||
constructor(
|
||||
private nukeType: NukeType,
|
||||
@@ -39,7 +41,11 @@ export class NukeExecution implements Execution {
|
||||
if (this.speed === -1) {
|
||||
this.speed = this.mg.config().defaultNukeSpeed();
|
||||
}
|
||||
this.pathFinder = new ParabolaPathFinder(mg);
|
||||
this.pathFinder = UniversalPathFinding.Parabola(mg, {
|
||||
increment: this.speed,
|
||||
distanceBasedHeight: this.nukeType !== UnitType.MIRVWarhead,
|
||||
directionUp: this.rocketDirectionUp,
|
||||
});
|
||||
}
|
||||
|
||||
public target(): Player | TerraNullius {
|
||||
@@ -123,13 +129,6 @@ export class NukeExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
this.src = spawn;
|
||||
this.pathFinder.computeControlPoints(
|
||||
spawn,
|
||||
this.dst,
|
||||
this.speed,
|
||||
this.nukeType !== UnitType.MIRVWarhead,
|
||||
this.rocketDirectionUp,
|
||||
);
|
||||
this.nuke = this.player.buildUnit(this.nukeType, spawn, {
|
||||
targetTile: this.dst,
|
||||
trajectory: this.getTrajectory(this.dst),
|
||||
@@ -186,13 +185,13 @@ export class NukeExecution implements Execution {
|
||||
}
|
||||
|
||||
// Move to next tile
|
||||
const nextTile = this.pathFinder.nextTile(this.speed);
|
||||
if (nextTile === true) {
|
||||
const result = this.pathFinder.next(this.src!, this.dst, this.speed);
|
||||
if (result.status === PathStatus.COMPLETE) {
|
||||
this.detonate();
|
||||
return;
|
||||
} else {
|
||||
} else if (result.status === PathStatus.NEXT) {
|
||||
this.updateNukeTargetable();
|
||||
this.nuke.move(nextTile);
|
||||
this.nuke.move(result.node);
|
||||
// Update index so SAM can interpolate future position
|
||||
this.nuke.setTrajectoryIndex(this.pathFinder.currentIndex());
|
||||
}
|
||||
@@ -206,7 +205,7 @@ export class NukeExecution implements Execution {
|
||||
const trajectoryTiles: TrajectoryTile[] = [];
|
||||
const targetRangeSquared =
|
||||
this.mg.config().defaultNukeTargetableRange() ** 2;
|
||||
const allTiles: TileRef[] = this.pathFinder.allTiles();
|
||||
const allTiles = this.pathFinder.findPath(this.src!, target) ?? [];
|
||||
for (const tile of allTiles) {
|
||||
trajectoryTiles.push({
|
||||
tile,
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { AirPathFinder } from "../pathfinding/PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { NukeType } from "../StatsSchemas";
|
||||
|
||||
export class SAMMissileExecution implements Execution {
|
||||
private active = true;
|
||||
private pathFinder: AirPathFinder;
|
||||
private pathFinder: SteppingPathFinder<TileRef>;
|
||||
private SAMMissile: Unit | undefined;
|
||||
private mg: Game;
|
||||
private speed: number = 0;
|
||||
@@ -27,7 +27,7 @@ export class SAMMissileExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathFinder = new AirPathFinder(mg, new PseudoRandom(mg.ticks()));
|
||||
this.pathFinder = PathFinding.Air(mg);
|
||||
this.mg = mg;
|
||||
this.speed = this.mg.config().defaultSamMissileSpeed();
|
||||
}
|
||||
@@ -55,11 +55,11 @@ export class SAMMissileExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.speed; i++) {
|
||||
const result = this.pathFinder.nextTile(
|
||||
const result = this.pathFinder.next(
|
||||
this.SAMMissile.tile(),
|
||||
this.targetTile,
|
||||
);
|
||||
if (result === true) {
|
||||
if (result.status === PathStatus.COMPLETE) {
|
||||
this.mg.displayMessage(
|
||||
"events_display.missile_intercepted",
|
||||
MessageType.SAM_HIT,
|
||||
@@ -76,8 +76,8 @@ export class SAMMissileExecution implements Execution {
|
||||
.stats()
|
||||
.bombIntercept(this._owner, this.target.type() as NukeType, 1);
|
||||
return;
|
||||
} else {
|
||||
this.SAMMissile.move(result);
|
||||
} else if (result.status === PathStatus.NEXT) {
|
||||
this.SAMMissile.move(result.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Execution, Game, Player, Unit, UnitType } from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { AirPathFinder } from "../pathfinding/PathFinding";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
|
||||
export class ShellExecution implements Execution {
|
||||
private active = true;
|
||||
private pathFinder: AirPathFinder;
|
||||
private pathFinder: SteppingPathFinder<TileRef>;
|
||||
private shell: Unit | undefined;
|
||||
private mg: Game;
|
||||
private destroyAtTick: number = -1;
|
||||
@@ -19,7 +20,7 @@ export class ShellExecution implements Execution {
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.pathFinder = new AirPathFinder(mg, new PseudoRandom(mg.ticks()));
|
||||
this.pathFinder = PathFinding.Air(mg);
|
||||
this.mg = mg;
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
}
|
||||
@@ -45,18 +46,18 @@ export class ShellExecution implements Execution {
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const result = this.pathFinder.nextTile(
|
||||
const result = this.pathFinder.next(
|
||||
this.shell.tile(),
|
||||
this.target.tile(),
|
||||
);
|
||||
if (result === true) {
|
||||
if (result.status === PathStatus.COMPLETE) {
|
||||
this.active = false;
|
||||
this.target.modifyHealth(-this.effectOnTarget(), this._owner);
|
||||
this.shell.setReachedTarget();
|
||||
this.shell.delete(false);
|
||||
return;
|
||||
} else {
|
||||
this.shell.move(result);
|
||||
} else if (result.status === PathStatus.NEXT) {
|
||||
this.shell.move(result.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PathFinder, PathFinders, PathStatus } from "../pathfinding/PathFinder";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { distSortUnit } from "../Util";
|
||||
|
||||
export class TradeShipExecution implements Execution {
|
||||
@@ -16,7 +17,7 @@ export class TradeShipExecution implements Execution {
|
||||
private mg: Game;
|
||||
private tradeShip: Unit | undefined;
|
||||
private wasCaptured = false;
|
||||
private pathFinder: PathFinder;
|
||||
private pathFinder: SteppingPathFinder<TileRef>;
|
||||
private tilesTraveled = 0;
|
||||
|
||||
constructor(
|
||||
@@ -27,7 +28,7 @@ export class TradeShipExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinders.Water(mg);
|
||||
this.pathFinder = PathFinding.Water(mg);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { targetTransportTile } from "../game/TransportShipUtils";
|
||||
import { PathFinder, PathFinders, PathStatus } from "../pathfinding/PathFinder";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { AttackExecution } from "./AttackExecution";
|
||||
|
||||
const malusForRetreat = 25;
|
||||
@@ -29,11 +30,10 @@ export class TransportShipExecution implements Execution {
|
||||
// TODO make private
|
||||
public path: TileRef[];
|
||||
private dst: TileRef | null;
|
||||
private dstShore: TileRef | null;
|
||||
|
||||
private boat: Unit;
|
||||
|
||||
private pathFinder: PathFinder;
|
||||
private pathFinder: SteppingPathFinder<TileRef>;
|
||||
|
||||
private originalOwner: Player;
|
||||
|
||||
@@ -70,7 +70,7 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.lastMove = ticks;
|
||||
this.mg = mg;
|
||||
this.pathFinder = PathFinders.Water(mg);
|
||||
this.pathFinder = PathFinding.Water(mg);
|
||||
|
||||
if (
|
||||
this.attacker.unitCount(UnitType.TransportShip) >=
|
||||
@@ -106,8 +106,8 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.startTroops = Math.min(this.startTroops, this.attacker.troops());
|
||||
|
||||
this.dstShore = targetTransportTile(this.mg, this.ref);
|
||||
if (this.dstShore === null) {
|
||||
this.dst = targetTransportTile(this.mg, this.ref);
|
||||
if (this.dst === null) {
|
||||
console.warn(
|
||||
`${this.attacker} cannot send ship to ${this.target}, cannot find attack tile`,
|
||||
);
|
||||
@@ -115,18 +115,9 @@ export class TransportShipExecution implements Execution {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dst = this.adjacentWater(this.dstShore);
|
||||
if (this.dst === null) {
|
||||
console.warn(
|
||||
`${this.attacker} cannot find water tile adjacent to destination`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const closestTileSrc = this.attacker.canBuild(
|
||||
UnitType.TransportShip,
|
||||
this.dstShore,
|
||||
this.dst,
|
||||
);
|
||||
if (closestTileSrc === false) {
|
||||
console.warn(`can't build transport ship`);
|
||||
@@ -152,21 +143,10 @@ export class TransportShipExecution implements Execution {
|
||||
|
||||
this.boat = this.attacker.buildUnit(UnitType.TransportShip, this.src, {
|
||||
troops: this.startTroops,
|
||||
targetTile: this.dst ?? undefined,
|
||||
});
|
||||
|
||||
// Move boat from shore to adjacent water for pathfinding
|
||||
const spawnWater = this.adjacentWater(this.src);
|
||||
if (spawnWater === null) {
|
||||
console.warn(`No adjacent water for transport ship spawn`);
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.boat.move(spawnWater);
|
||||
|
||||
if (this.dstShore !== null) {
|
||||
this.boat.setTargetTile(this.dstShore);
|
||||
if (this.dst !== null) {
|
||||
this.boat.setTargetTile(this.dst);
|
||||
} else {
|
||||
this.boat.setTargetTile(undefined);
|
||||
}
|
||||
@@ -222,7 +202,6 @@ export class TransportShipExecution implements Execution {
|
||||
if (this.mg.owner(this.src!) !== this.attacker) {
|
||||
// Use bestTransportShipSpawn, not canBuild because of its max boats check etc
|
||||
const newSrc = this.attacker.bestTransportShipSpawn(this.dst);
|
||||
|
||||
if (newSrc === false) {
|
||||
this.src = null;
|
||||
} else {
|
||||
@@ -239,19 +218,10 @@ export class TransportShipExecution implements Execution {
|
||||
this.active = false;
|
||||
return;
|
||||
} else {
|
||||
this.dstShore = this.src;
|
||||
const retreatWater = this.adjacentWater(this.src);
|
||||
if (retreatWater === null) {
|
||||
console.warn(`No adjacent water for retreat destination`);
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
this.dst = retreatWater;
|
||||
this.dst = this.src;
|
||||
|
||||
if (this.boat.targetTile() !== this.dstShore) {
|
||||
this.boat.setTargetTile(this.dstShore!);
|
||||
if (this.boat.targetTile() !== this.dst) {
|
||||
this.boat.setTargetTile(this.dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,7 +229,7 @@ export class TransportShipExecution implements Execution {
|
||||
const result = this.pathFinder.next(this.boat.tile(), this.dst);
|
||||
switch (result.status) {
|
||||
case PathStatus.COMPLETE:
|
||||
if (this.mg.owner(this.dstShore!) === this.attacker) {
|
||||
if (this.mg.owner(this.dst) === this.attacker) {
|
||||
const deaths = this.boat.troops() * (malusForRetreat / 100);
|
||||
const survivors = this.boat.troops() - deaths;
|
||||
this.attacker.addTroops(survivors);
|
||||
@@ -281,7 +251,7 @@ export class TransportShipExecution implements Execution {
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.attacker.conquer(this.dstShore!);
|
||||
this.attacker.conquer(this.dst);
|
||||
if (this.target.isPlayer() && this.attacker.isFriendly(this.target)) {
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
} else {
|
||||
@@ -290,7 +260,7 @@ export class TransportShipExecution implements Execution {
|
||||
this.boat.troops(),
|
||||
this.attacker,
|
||||
this.targetID,
|
||||
this.dstShore!,
|
||||
this.dst,
|
||||
false,
|
||||
),
|
||||
);
|
||||
@@ -308,13 +278,18 @@ export class TransportShipExecution implements Execution {
|
||||
break;
|
||||
case PathStatus.PENDING:
|
||||
break;
|
||||
case PathStatus.NOT_FOUND:
|
||||
case PathStatus.NOT_FOUND: {
|
||||
// TODO: add to poisoned port list
|
||||
console.warn(`path not found to dst`);
|
||||
const map = this.mg.map();
|
||||
const boatTile = this.boat.tile();
|
||||
console.warn(
|
||||
`TransportShip path not found: boat@(${map.x(boatTile)},${map.y(boatTile)}) -> dst@(${map.x(this.dst)},${map.y(this.dst)}), attacker=${this.attacker.id()}, target=${this.targetID}`,
|
||||
);
|
||||
this.attacker.addTroops(this.boat.troops());
|
||||
this.boat.delete(false);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,17 +300,4 @@ export class TransportShipExecution implements Execution {
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
private adjacentWater(tile: TileRef): TileRef | null {
|
||||
if (this.mg.isWater(tile)) {
|
||||
return tile;
|
||||
}
|
||||
|
||||
for (const neighbor of this.mg.neighbors(tile)) {
|
||||
if (this.mg.isWater(neighbor)) {
|
||||
return neighbor;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
UnitType,
|
||||
} from "../game/Game";
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { PathFinder, PathFinders, PathStatus } from "../pathfinding/PathFinder";
|
||||
import { PathFinding } from "../pathfinding/PathFinder";
|
||||
import { PathStatus, SteppingPathFinder } from "../pathfinding/types";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { ShellExecution } from "./ShellExecution";
|
||||
|
||||
@@ -16,7 +17,7 @@ export class WarshipExecution implements Execution {
|
||||
private random: PseudoRandom;
|
||||
private warship: Unit;
|
||||
private mg: Game;
|
||||
private pathfinder: PathFinder;
|
||||
private pathfinder: SteppingPathFinder<TileRef>;
|
||||
private lastShellAttack = 0;
|
||||
private alreadySentShell = new Set<Unit>();
|
||||
|
||||
@@ -26,7 +27,7 @@ export class WarshipExecution implements Execution {
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
this.mg = mg;
|
||||
this.pathfinder = PathFinders.Water(mg);
|
||||
this.pathfinder = PathFinding.Water(mg);
|
||||
this.random = new PseudoRandom(mg.ticks());
|
||||
if (isUnit(this.input)) {
|
||||
this.warship = this.input;
|
||||
@@ -193,9 +194,10 @@ export class WarshipExecution implements Execution {
|
||||
case PathStatus.PENDING:
|
||||
this.warship.touch();
|
||||
break;
|
||||
case PathStatus.NOT_FOUND:
|
||||
case PathStatus.NOT_FOUND: {
|
||||
console.log(`path not found to target`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,10 +225,10 @@ export class WarshipExecution implements Execution {
|
||||
case PathStatus.PENDING:
|
||||
this.warship.touch();
|
||||
return;
|
||||
case PathStatus.NOT_FOUND:
|
||||
console.warn(`path not found to target tile`);
|
||||
this.warship.setTargetTile(undefined);
|
||||
case PathStatus.NOT_FOUND: {
|
||||
console.log(`path not found to target`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +245,10 @@ export class WarshipExecution implements Execution {
|
||||
const maxAttemptBeforeExpand: number = 500;
|
||||
let attempts: number = 0;
|
||||
let expandCount: number = 0;
|
||||
|
||||
// Get warship's water component for connectivity check
|
||||
const warshipComponent = this.mg.getWaterComponent(this.warship.tile());
|
||||
|
||||
while (expandCount < 3) {
|
||||
const x =
|
||||
this.mg.x(this.warship.patrolTile()!) +
|
||||
@@ -267,6 +273,20 @@ export class WarshipExecution implements Execution {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Check water component connectivity
|
||||
if (
|
||||
warshipComponent !== null &&
|
||||
!this.mg.hasWaterComponent(tile, warshipComponent)
|
||||
) {
|
||||
attempts++;
|
||||
if (attempts === maxAttemptBeforeExpand) {
|
||||
expandCount++;
|
||||
attempts = 0;
|
||||
warshipPatrolRange =
|
||||
warshipPatrolRange + Math.floor(warshipPatrolRange / 2);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
console.warn(
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
UnitType,
|
||||
} from "../../game/Game";
|
||||
import { TileRef, euclDistFN } from "../../game/GameMap";
|
||||
import { ParabolaPathFinder } from "../../pathfinding/PathFinding";
|
||||
import { UniversalPathFinding } from "../../pathfinding/PathFinder";
|
||||
import { PseudoRandom } from "../../PseudoRandom";
|
||||
import { assertNever, boundingBoxTiles } from "../../Util";
|
||||
import { NukeExecution } from "../NukeExecution";
|
||||
@@ -456,20 +456,14 @@ export class NationNukeBehavior {
|
||||
spawnTile: TileRef,
|
||||
targetTile: TileRef,
|
||||
): boolean {
|
||||
const pathFinder = new ParabolaPathFinder(this.game);
|
||||
const speed = this.game.config().defaultNukeSpeed();
|
||||
const distanceBasedHeight = true; // Atom/Hydrogen bombs use distance-based height
|
||||
const rocketDirectionUp = true; // AI nukes always go "up" for now
|
||||
const pathFinder = UniversalPathFinding.Parabola(this.game, {
|
||||
increment: speed,
|
||||
distanceBasedHeight: true, // Atom/Hydrogen bombs use distance-based height
|
||||
directionUp: true, // AI nukes always go "up" for now
|
||||
});
|
||||
|
||||
pathFinder.computeControlPoints(
|
||||
spawnTile,
|
||||
targetTile,
|
||||
speed,
|
||||
distanceBasedHeight,
|
||||
rocketDirectionUp,
|
||||
);
|
||||
|
||||
const trajectory = pathFinder.allTiles();
|
||||
const trajectory = pathFinder.findPath(spawnTile, targetTile) ?? [];
|
||||
if (trajectory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user