diff --git a/src/core/game/RailNetworkImpl.ts b/src/core/game/RailNetworkImpl.ts index d3aef952b..b56599beb 100644 --- a/src/core/game/RailNetworkImpl.ts +++ b/src/core/game/RailNetworkImpl.ts @@ -4,6 +4,7 @@ import { Game, Unit, UnitType } from "./Game"; import { TileRef } from "./GameMap"; import { RailNetwork } from "./RailNetwork"; import { Railroad } from "./Railroad"; +import { RailSpatialGrid } from "./RailroadSpatialGrid"; import { Cluster, TrainStation } from "./TrainStation"; /** @@ -81,12 +82,17 @@ export function createRailNetwork(game: Game): RailNetwork { export class RailNetworkImpl implements RailNetwork { private maxConnectionDistance: number = 4; + private stationRadius: number = 3; + private gridCellSize: number = 4; + private railGrid: RailSpatialGrid; constructor( private game: Game, private _stationManager: StationManager, private pathService: RailPathFinderService, - ) {} + ) { + this.railGrid = new RailSpatialGrid(game, this.gridCellSize); // 4x4 tiles spatial grid + } stationManager(): StationManager { return this._stationManager; @@ -94,7 +100,9 @@ export class RailNetworkImpl implements RailNetwork { connectStation(station: TrainStation) { this._stationManager.addStation(station); - this.connectToNearbyStations(station); + if (!this.connectToExistingRails(station)) { + this.connectToNearbyStations(station); + } } removeStation(unit: Unit): void { @@ -126,6 +134,59 @@ export class RailNetworkImpl implements RailNetwork { return this.pathService.findStationsPath(from, to); } + private connectToExistingRails(station: TrainStation): boolean { + const rails = this.railGrid.query(station.tile(), this.stationRadius); + + const editedClusters = new Set(); + for (const rail of rails) { + const from = rail.from; + const to = rail.to; + const closestRailIndex = rail.getClosestTileIndex( + this.game, + station.tile(), + ); + if (closestRailIndex === 0 || closestRailIndex >= rail.tiles.length) { + continue; + } + + // Disconnect current rail as it will become invalid + from.removeRailroad(rail); + to.removeRailroad(rail); + this.railGrid.unregister(rail); + + const newRailFrom = new Railroad( + from, + station, + rail.tiles.slice(0, closestRailIndex), + ); + const newRailTo = new Railroad( + station, + to, + rail.tiles.slice(closestRailIndex), + ); + + // New station is connected to both new rails + station.addRailroad(newRailFrom); + station.addRailroad(newRailTo); + // From and to are connected to the new segments + from.addRailroad(newRailFrom); + to.addRailroad(newRailTo); + + this.railGrid.register(newRailTo); + this.railGrid.register(newRailFrom); + const cluster = from.getCluster(); + if (cluster) { + cluster.addStation(station); + editedClusters.add(cluster); + } + } + // If multiple clusters own the new station, merge them into a single cluster + if (editedClusters.size > 1) { + this.mergeClusters(editedClusters); + } + return editedClusters.size !== 0; + } + private connectToNearbyStations(station: TrainStation) { const neighbors = this.game.nearbyUnits( station.tile(), @@ -176,6 +237,7 @@ export class RailNetworkImpl implements RailNetwork { private disconnectFromNetwork(station: TrainStation) { for (const rail of station.getRailroads()) { rail.delete(this.game); + this.railGrid.unregister(rail); } station.clearRailroads(); const cluster = station.getCluster(); @@ -198,6 +260,7 @@ export class RailNetworkImpl implements RailNetwork { this.game.addExecution(new RailroadExecution(railRoad)); from.addRailroad(railRoad); to.addRailroad(railRoad); + this.railGrid.register(railRoad); return true; } return false; diff --git a/src/core/game/Railroad.ts b/src/core/game/Railroad.ts index 8b0f3086a..00b5d9c37 100644 --- a/src/core/game/Railroad.ts +++ b/src/core/game/Railroad.ts @@ -23,6 +23,26 @@ export class Railroad { this.from.removeRailroad(this); this.to.removeRailroad(this); } + + getClosestTileIndex(game: Game, to: TileRef): number { + if (this.tiles.length === 0) return -1; + const toX = game.x(to); + const toY = game.y(to); + let closestIndex = 0; + let minDistSquared = Infinity; + for (let i = 0; i < this.tiles.length; i++) { + const tile = this.tiles[i]; + const dx = game.x(tile) - toX; + const dy = game.y(tile) - toY; + const distSquared = dx * dx + dy * dy; + + if (distSquared < minDistSquared) { + minDistSquared = distSquared; + closestIndex = i; + } + } + return closestIndex; + } } export function getOrientedRailroad( diff --git a/src/core/game/RailroadSpatialGrid.ts b/src/core/game/RailroadSpatialGrid.ts new file mode 100644 index 000000000..27e57fcae --- /dev/null +++ b/src/core/game/RailroadSpatialGrid.ts @@ -0,0 +1,97 @@ +import { GameMap, TileRef } from "./GameMap"; +import { Railroad } from "./Railroad"; + +export class RailSpatialGrid { + private cells = new Map>(); + // Quick access to avoid iterating over the cells + private railToCells = new Map>(); + + constructor( + private game: GameMap, + private cellSize: number, + ) { + if (cellSize <= 0) { + throw new Error("cellSize must be > 0"); + } + } + + register(rail: Railroad) { + // Defensive: avoid double-registration but it should never happen + this.unregister(rail); + + const railCells = new Set(); + + for (const tile of rail.tiles) { + const { cx, cy } = this.cellOf(this.game.x(tile), this.game.y(tile)); + const k = this.key(cx, cy); + if (railCells.has(k)) continue; + + let set = this.cells.get(k); + if (!set) { + set = new Set(); + this.cells.set(k, set); + } + railCells.add(k); + set.add(rail); + } + + if (railCells.size > 0) { + this.railToCells.set(rail, railCells); + } + } + + unregister(rail: Railroad) { + const keys = this.railToCells.get(rail); + if (!keys) return; + + for (const k of keys) { + const set = this.cells.get(k); + if (!set) continue; + set.delete(rail); + + if (set.size === 0) { + this.cells.delete(k); + } + } + + this.railToCells.delete(rail); + } + + query(tile: TileRef, radius: number): Set { + const x = this.game.x(tile); + const y = this.game.y(tile); + + const minX = x - radius; + const minY = y - radius; + const maxX = x + radius; + const maxY = y + radius; + + const c0 = this.cellOf(minX, minY); + const c1 = this.cellOf(maxX, maxY); + + const result = new Set(); + + for (let cx = c0.cx; cx <= c1.cx; cx++) { + for (let cy = c0.cy; cy <= c1.cy; cy++) { + const set = this.cells.get(this.key(cx, cy)); + if (!set) continue; + for (const rail of set) { + result.add(rail); + } + } + } + + return result; + } + + private key(cx: number, cy: number): string { + return `${cx}:${cy}`; + } + + private cellOf(x: number, y: number): { cx: number; cy: number } { + return { + cx: Math.floor(x / this.cellSize), + cy: Math.floor(y / this.cellSize), + }; + } +} diff --git a/tests/core/game/RailNetwork.test.ts b/tests/core/game/RailNetwork.test.ts index d77ca10dd..fc39db81d 100644 --- a/tests/core/game/RailNetwork.test.ts +++ b/tests/core/game/RailNetwork.test.ts @@ -71,6 +71,8 @@ describe("RailNetworkImpl", () => { trainStationMinRange: () => 10, railroadMaxSize: () => 100, }), + x: vi.fn(() => 0), + y: vi.fn(() => 0), }; network = new RailNetworkImpl(game, stationManager, pathService);