diff --git a/TODO.txt b/TODO.txt index 5674c8b76..aa8d16d6d 100644 --- a/TODO.txt +++ b/TODO.txt @@ -202,9 +202,10 @@ * create async pathfinder DONE 11/29/2024 * captured tradeships use async pathfinder DONE 11/29/2024 * BUG: trade ships not building DONE 11/29/2024 +* make mini map for path finding DONE 11/29/2024 * have NPCs build destroyers and battleships -* spread out calculate clusters * add radiation from nuke +* spread out calculate clusters * add defense post * NPC has relations * only show units you can build in the build menu diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 7f0fc0377..ab83e815d 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -143,13 +143,17 @@ export class UnitLayer implements Layer { this.clearCell(t.cell()); }); if (event.unit.isActive()) { - bfs(event.unit.tile(), dist(event.unit.tile(), 4)).forEach( - t => { - if (trail.has(t)) { - this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 150); + try { + bfs(event.unit.tile(), dist(event.unit.tile(), 4)).forEach( + t => { + if (trail.has(t)) { + this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 150); + } } - } - ); + ); + } catch { + console.log('uh oh') + } bfs(event.unit.tile(), dist(event.unit.tile(), 2)) .forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255)); bfs(event.unit.tile(), dist(event.unit.tile(), 1)) diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 3dbdaa8da..3126b8180 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -96,30 +96,26 @@ export class TransportShipExecution implements Execution { } this.lastMove = ticks - - if (this.boat.tile() == this.dst) { - if (this.dst.owner() == this.attacker) { - this.attacker.addTroops(this.troops) - this.boat.delete() - this.active = false - return - } - if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) { - this.target.addTroops(this.troops) - } else { - this.attacker.conquer(this.dst) - this.mg.addExecution( - new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst.cell(), null, false) - ) - } - this.boat.delete() - this.active = false - return - } - const result = this.pathFinder.nextTile(this.boat.tile(), this.dst) switch (result.type) { case PathFindResultType.Completed: + if (this.dst.owner() == this.attacker) { + this.attacker.addTroops(this.troops) + this.boat.delete() + this.active = false + return + } + if (this.target.isPlayer() && this.attacker.isAlliedWith(this.target)) { + this.target.addTroops(this.troops) + } else { + this.attacker.conquer(this.dst) + this.mg.addExecution( + new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst.cell(), null, false) + ) + } + this.boat.delete() + this.active = false + return case PathFindResultType.NextTile: this.boat.move(result.tile) break diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 683236759..a4de166a6 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -135,6 +135,8 @@ export class PlayerInfo { export interface TerrainMap { terrain(cell: Cell): TerrainTile neighbors(terrainTile: TerrainTile): TerrainTile[] + width(): number + height(): number } export interface TerrainTile extends SearchNode { diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts index 54fd1c00f..decf30127 100644 --- a/src/core/game/TerrainMapLoader.ts +++ b/src/core/game/TerrainMapLoader.ts @@ -167,6 +167,33 @@ export async function loadTerrainMap(map: GameMap): Promise { return m } + +export function createMiniMap(tm: TerrainMap): TerrainMap { + // Create 2D array properly with correct dimensions + const miniMap: TerrainTileImpl[][] = Array(Math.floor(tm.width() / 2)) + .fill(null) + .map(() => Array(Math.floor(tm.height() / 2)).fill(null)); + + for (let x = 0; x < tm.width(); x++) { + for (let y = 0; y < tm.height(); y++) { + const tile = tm.terrain(new Cell(x, y)) as TerrainTileImpl; + const miniX = Math.floor(x / 2); + const miniY = Math.floor(y / 2); + + if (miniMap[miniX][miniY] == null || miniMap[miniX][miniY].terrainType() != TerrainType.Ocean) { + miniMap[miniX][miniY] = new TerrainTileImpl(tile.terrainType(), new Cell(miniX, miniY)); + miniMap[miniX][miniY].shoreline = tile.shoreline; + miniMap[miniX][miniY].magnitude = tile.magnitude; + miniMap[miniX][miniY].ocean = tile.ocean; + miniMap[miniX][miniY].land = tile.land; + } + } + } + + return new TerrainMapImpl(miniMap, 0, null); +} + + function logBinaryAsAscii(data: string, length: number = 8) { console.log('Binary data (1 = set bit, 0 = unset bit):'); for (let i = 0; i < Math.min(length, data.length); i++) { diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 05d65382d..9e374227a 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -20,6 +20,9 @@ export class UnitImpl implements MutableUnit { } move(tile: Tile): void { + if(tile == null) { + throw new Error("tile cannot be null") + } const oldTile = this._tile; this._tile = tile; this.g.fireUnitUpdateEvent(this, oldTile); diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index dd131b40b..c096c373c 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -1,6 +1,6 @@ // pathfinding.ts import { Cell, GameMap, TerrainMap, TerrainTile, TerrainType } from "../game/Game"; -import { loadTerrainMap } from "../game/TerrainMapLoader"; +import { createMiniMap, loadTerrainMap } from "../game/TerrainMapLoader"; import { PriorityQueue } from "@datastructures-js/priority-queue"; import { SerialAStar } from "../pathfinding/SerialAStar"; import { PathFindResultType, SearchNode } from "../pathfinding/AStar"; @@ -10,11 +10,16 @@ let searches = new PriorityQueue((a: Search, b: Search) => (a.deadline - let processingInterval: number | null = null; let isProcessingSearch = false +interface Point { + x: number + y: number +} interface Search { aStar: SerialAStar, deadline: number - requestId: string + requestId: string, + end: Point } interface SearchRequest { @@ -22,8 +27,8 @@ interface SearchRequest { currentTick: number // duration in ticks duration: number - start: { x: number, y: number }, - end: { x: number, y: number } + start: Point + end: Point } self.onmessage = (e) => { @@ -38,15 +43,16 @@ self.onmessage = (e) => { }; function initializeMap(data: { gameMap: GameMap }) { - terrainMapPromise = loadTerrainMap(data.gameMap) + terrainMapPromise = loadTerrainMap(data.gameMap).then(tm => createMiniMap(tm)) self.postMessage({ type: 'initialized' }); processingInterval = setInterval(computeSearches, .1) as unknown as number; } function findPath(terrainMap: TerrainMap, req: SearchRequest) { + console.log(`terrain map height: ${terrainMap.height()}`) const aStar = new SerialAStar( - terrainMap.terrain(new Cell(req.start.x, req.start.y)), - terrainMap.terrain(new Cell(req.end.x, req.end.y)), + terrainMap.terrain(new Cell(Math.floor(req.start.x / 2), Math.floor(req.start.y / 2))), + terrainMap.terrain(new Cell(Math.floor(req.end.x / 2), Math.floor(req.end.y / 2))), (sn: SearchNode) => (sn as TerrainTile).terrainType() == TerrainType.Ocean, (sn: SearchNode): SearchNode[] => terrainMap.neighbors((sn as TerrainTile)), 10_000, @@ -56,7 +62,8 @@ function findPath(terrainMap: TerrainMap, req: SearchRequest) { searches.enqueue({ aStar: aStar, deadline: req.currentTick + req.duration, - requestId: req.requestId + requestId: req.requestId, + end: req.end }) } @@ -75,10 +82,12 @@ function computeSearches() { const search = searches.dequeue() switch (search.aStar.compute()) { case PathFindResultType.Completed: + const path = upscalePath(search.aStar.reconstructPath().map(sn => ({ x: sn.cell().x, y: sn.cell().y }))) + path.push(search.end) self.postMessage({ type: 'pathFound', requestId: search.requestId, - path: search.aStar.reconstructPath().map(sn => ({ x: sn.cell().x, y: sn.cell().y })) + path: path }); break; @@ -97,4 +106,45 @@ function computeSearches() { } finally { isProcessingSearch = false } -} \ No newline at end of file +} + +function upscalePath(path: Point[], scaleFactor: number = 2): Point[] { + // Scale up each point + const scaledPath = path.map(point => ({ + x: point.x * scaleFactor, + y: point.y * scaleFactor + })); + + const smoothPath: Point[] = []; + + for (let i = 0; i < scaledPath.length - 1; i++) { + const current = scaledPath[i]; + const next = scaledPath[i + 1]; + + // Add the current point + smoothPath.push(current); + + // Always interpolate between scaled points + const dx = next.x - current.x; + const dy = next.y - current.y; + + // Calculate number of steps needed + const distance = Math.max(Math.abs(dx), Math.abs(dy)); + const steps = distance; + + // Add intermediate points + for (let step = 1; step < steps; step++) { + smoothPath.push({ + x: Math.round(current.x + (dx * step) / steps), + y: Math.round(current.y + (dy * step) / steps) + }); + } + } + + // Add the last point + if (scaledPath.length > 0) { + smoothPath.push(scaledPath[scaledPath.length - 1]); + } + + return smoothPath; +}