diff --git a/TODO.txt b/TODO.txt index b09db5ee1..c0b6f9937 100644 --- a/TODO.txt +++ b/TODO.txt @@ -40,15 +40,15 @@ * only put imageDataOnce, draw territories on top DONE 8/23/2024 * have boats not get close to shore DONE 8/23/2024 * improve terrain colors DONE 8/23/2024 -* BUG: boat doesn't work if on lake if other player not on same lake -* try vintage theme -* BUG: boat doesn't work if on lake if other player not on same lake +* BUG: boat doesn't work if on lake if other player not on same lake DONE 8/23/2024 * Allow boats to attack TerraNullius +* improve menu +* try vintage theme * add shader to dim border * remove player.info() -* improve menu * give time to (re) spawn at start of game * BUG: ocean is considered TerraNullius ? * BUG: fix hotreload (priority queue breaks it) * PERF: use hierarchical a* search for boats -* Add terrain elevation to map \ No newline at end of file +* Add terrain elevation to map +* Boats can go diagonally \ No newline at end of file diff --git a/resources/World.bin b/resources/World.bin index 07e2b70d7..72b735041 100644 Binary files a/resources/World.bin and b/resources/World.bin differ diff --git a/resources/maps/World.png b/resources/maps/World.png index d2c590718..921044c21 100644 Binary files a/resources/maps/World.png and b/resources/maps/World.png differ diff --git a/resources/maps/WorldOriginal.png b/resources/maps/WorldOriginal.png new file mode 100644 index 000000000..d2c590718 Binary files /dev/null and b/resources/maps/WorldOriginal.png differ diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index f78316408..df192cc47 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -7,6 +7,7 @@ import {GameRenderer} from "./graphics/GameRenderer"; import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler" import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas"; import {TerrainMap} from "../core/TerrainMapLoader"; +import {bfs, manhattanDist} from "../core/Util"; @@ -189,10 +190,17 @@ export class ClientGame { const owner = tile.owner() const targetID = owner.isPlayer() ? owner.id() : null if (tile.owner() != this.myPlayer && tile.isLand()) { + // const ocean = Array.from(bfs(tile, 4)) + // .filter(t => t.isOcean) + // .sort((a, b) => manhattanDist(tile.cell(), a.cell()) - manhattanDist(tile.cell(), b.cell())) + // if (ocean.length > 0) { + // this.sendBoatAttackIntent(targetID, cell, this.config.player().boatAttackAmount(this.myPlayer, owner)) + // return + // } + if (this.myPlayer.sharesBorderWith(tile.owner())) { this.sendAttackIntent(targetID, cell, this.config.player().attackAmount(this.myPlayer, owner)) } else if (owner.isPlayer()) { - // TODO verify on ocean console.log('going to send boat') this.sendBoatAttackIntent(targetID, cell, this.config.player().boatAttackAmount(this.myPlayer, owner)) } diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index bd7792f75..cf357af15 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -3,7 +3,7 @@ import {Cell, Game, PlayerEvent, Tile, TileEvent, Player, Execution, BoatEvent} import {Theme} from "../../core/configuration/Config"; import {DragEvent, ZoomEvent} from "../InputHandler"; import {NameRenderer} from "./NameRenderer"; -import {manhattanDist} from "../../core/Util"; +import {bfs, manhattanDist} from "../../core/Util"; import {PseudoRandom} from "../../core/PseudoRandom"; @@ -146,30 +146,14 @@ export class GameRenderer { } boatEvent(event: BoatEvent) { - this.bfs(event.oldTile, 2).forEach(t => this.paintTerritory(t)) + bfs(event.oldTile, 2).forEach(t => this.paintTerritory(t)) if (event.boat.isActive()) { - this.bfs(event.boat.tile(), 2).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().id()))) - this.bfs(event.boat.tile(), 1).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().id()))) + bfs(event.boat.tile(), 2).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().id()))) + bfs(event.boat.tile(), 1).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().id()))) } } - private bfs(tile: Tile, dist: number): Set { - const seen = new Set - const q: Tile[] = [] - q.push(tile) - while (q.length > 0) { - const curr = q.pop() - seen.add(curr) - for (const n of curr.neighbors()) { - if (!seen.has(n) && manhattanDist(tile.cell(), n.cell()) <= dist) { - q.push(n) - } - } - } - return seen - } - resize(width: number, height: number): void { this.canvas.width = Math.ceil(width / window.devicePixelRatio); this.canvas.height = Math.ceil(height / window.devicePixelRatio); diff --git a/src/client/graphics/NameBoxCalculator.ts b/src/client/graphics/NameBoxCalculator.ts index d251f5e9f..dc0e37acc 100644 --- a/src/client/graphics/NameBoxCalculator.ts +++ b/src/client/graphics/NameBoxCalculator.ts @@ -74,7 +74,7 @@ export function createGrid(game: Game, player: Player, boundingBox: {min: Point; const cell = new Cell(x * scalingFactor, y * scalingFactor); if (game.isOnMap(cell)) { const tile = game.tile(cell); - grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = tile.owner() === player; // TODO: okay if lake + grid[x - scaledBoundingBox.min.x][y - scaledBoundingBox.min.y] = tile.isLake() || tile.owner() === player; // TODO: okay if lake } } } diff --git a/src/core/Game.ts b/src/core/Game.ts index d124680fd..c96a9da38 100644 --- a/src/core/Game.ts +++ b/src/core/Game.ts @@ -23,8 +23,8 @@ export interface ExecutionView { } export interface Execution extends ExecutionView { - init(mg: MutableGame, ticks: number) - tick(ticks: number) + init(mg: MutableGame, ticks: number): void + tick(ticks: number): void owner(): MutablePlayer } @@ -42,6 +42,8 @@ export interface Tile { isShore(): boolean isWater(): boolean isShorelineWater(): boolean + isOcean(): boolean + isLake(): boolean magnitude(): number owner(): Player | TerraNullius hasOwner(): boolean diff --git a/src/core/GameImpl.ts b/src/core/GameImpl.ts index 346036541..121e13f25 100644 --- a/src/core/GameImpl.ts +++ b/src/core/GameImpl.ts @@ -19,6 +19,12 @@ class TileImpl implements Tile { private readonly _cell: Cell, private readonly _terrain: Terrain ) { } + isLake(): boolean { + return !this.isLand() && !this.isOcean() + } + isOcean(): boolean { + return this._terrain.ocean + } magnitude(): number { return this._terrain.magnitude } diff --git a/src/core/TerrainMapLoader.ts b/src/core/TerrainMapLoader.ts index dc26b83e0..d9348e05a 100644 --- a/src/core/TerrainMapLoader.ts +++ b/src/core/TerrainMapLoader.ts @@ -24,6 +24,7 @@ export enum TerrainType { export class Terrain { public shoreline: boolean = false + public ocean: boolean = false public magnitude: number = 0 constructor(public type: TerrainType) { } } @@ -55,10 +56,12 @@ export function loadTerrainMap(): TerrainMap { const packedByte = fileData.charCodeAt(4 + y * width + x); // +4 to skip dimension bytes const type = (packedByte & 0b10000000) ? TerrainType.Land : TerrainType.Water; const shoreline = !!(packedByte & 0b01000000); - const magnitude = packedByte & 0b00111111; + const ocean = !!(packedByte & 0b00100000); + const magnitude = packedByte & 0b00011111; terrain[x][y] = new Terrain(type); terrain[x][y].shoreline = shoreline; + terrain[x][y].ocean = ocean; terrain[x][y].magnitude = magnitude; } } diff --git a/src/core/Ticker.ts b/src/core/Ticker.ts deleted file mode 100644 index e18569940..000000000 --- a/src/core/Ticker.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {EventBus, GameEvent} from "./EventBus"; -import {Config} from "./configuration/Config"; - -export class TickEvent implements GameEvent { - constructor(public readonly tickCount: number) { } -} - -export class Ticker { - private ticker: NodeJS.Timeout; - private tickCount: number; - - constructor(private tickInterval: number, private eventBus: EventBus) { - - } - - start() { - this.tickCount = 0; - this.ticker = setInterval(() => this.tick(), this.tickInterval); - } - - stop() { - clearInterval(this.ticker); - } - - private tick() { - this.eventBus.emit(new TickEvent(this.tickCount)) - this.tickCount++; - } - - getTickCount(): number { - return this.tickCount; - } -} \ No newline at end of file diff --git a/src/core/Util.ts b/src/core/Util.ts index 1ddd5fe43..1838bd66c 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -1,4 +1,4 @@ -import {Cell} from "./Game"; +import {Cell, Tile} from "./Game"; export function manhattanDist(c1: Cell, c2: Cell): number { return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y); @@ -6,4 +6,20 @@ export function manhattanDist(c1: Cell, c2: Cell): number { export function within(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); +} + +export function bfs(tile: Tile, dist: number): Set { + const seen = new Set + const q: Tile[] = [] + q.push(tile) + while (q.length > 0) { + const curr = q.pop() + seen.add(curr) + for (const n of curr.neighbors()) { + if (!seen.has(n) && manhattanDist(tile.cell(), n.cell()) <= dist) { + q.push(n) + } + } + } + return seen } \ No newline at end of file diff --git a/src/core/execution/BoatAttackExecution.ts b/src/core/execution/BoatAttackExecution.ts index 861fc2487..6d617b223 100644 --- a/src/core/execution/BoatAttackExecution.ts +++ b/src/core/execution/BoatAttackExecution.ts @@ -117,7 +117,7 @@ export class BoatAttackExecution implements Execution { } private closestShoreTileToTarget(player: Player, target: Cell): Tile | null { - const shoreTiles = Array.from(player.borderTiles()).filter(t => t.onShore()) + const shoreTiles = Array.from(player.borderTiles()).filter(t => t.onShore() && t.neighbors().filter(n => n.isOcean()).length > 0) if (shoreTiles.length == 0) { return null } diff --git a/src/scripts/TerrainMapGenerator.ts b/src/scripts/TerrainMapGenerator.ts index b0579bdc9..cd463eddd 100644 --- a/src/scripts/TerrainMapGenerator.ts +++ b/src/scripts/TerrainMapGenerator.ts @@ -39,6 +39,7 @@ export enum TerrainType { export class Terrain { public shoreline: boolean = false public magnitude: number = 0 + public ocean: boolean constructor(public type: TerrainType) { } } @@ -57,12 +58,12 @@ export async function loadTerrainMap(): Promise { for (let x = 0; x < img.width; x++) { for (let y = 0; y < img.height; y++) { const color = img.getPixelRGBA(x, y); - const red = (color >> 24) & 0xff; + const alpha = color & 0xff; - if (red > 100) { - terrain[x][y] = new Terrain(TerrainType.Land) - } else { + if (alpha < 20) { // transparent terrain[x][y] = new Terrain(TerrainType.Water); + } else { + terrain[x][y] = new Terrain(TerrainType.Land) } } } @@ -70,6 +71,7 @@ export async function loadTerrainMap(): Promise { const shorelineWaters = processShore(terrain) processDistToLand(shorelineWaters, terrain) + processOcean(terrain) const packed = packTerrain(terrain) const outputPath = path.join(__dirname, '..', '..', 'resources', 'World.bin'); fs.writeFile(outputPath, packed); @@ -163,7 +165,10 @@ function packTerrain(map: Terrain[][]): Uint8Array { if (terrain.shoreline) { packedByte |= 0b01000000; } - packedByte |= Math.min(Math.ceil(terrain.magnitude / 2), 63); + if (terrain.ocean) { + packedByte |= 0b00100000; + } + packedByte |= Math.min(Math.ceil(terrain.magnitude / 2), 31); packedData[4 + y * width + x] = packedByte; } @@ -172,6 +177,34 @@ function packTerrain(map: Terrain[][]): Uint8Array { return packedData; } +function processOcean(map: Terrain[][]) { + const queue: Coord[] = [{x: 0, y: 0}]; + const visited = new Set(); + + while (queue.length > 0) { + const coord = queue.shift()!; + const key = `${coord.x},${coord.y}`; + + if (visited.has(key)) continue; + visited.add(key); + + const terrain = map[coord.x][coord.y]; + if (terrain.type === TerrainType.Water) { + terrain.ocean = true; + + // Check neighbors + for (const [dx, dy] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) { + const newX = coord.x + dx; + const newY = coord.y + dy; + + if (newX >= 0 && newX < map.length && newY >= 0 && newY < map[0].length) { + queue.push({x: newX, y: newY}); + } + } + } + } +} + function logBinaryAsBits(data: Uint8Array, length: number = 8) { const bits = Array.from(data.slice(0, length)) .map(b => b.toString(2).padStart(8, '0'))