From 3ac8dbddc574a9615c87eae94302b08d7d13f65b Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 9 Nov 2024 13:32:25 -0800 Subject: [PATCH 01/25] create unit layer --- TODO.txt | 9 +- src/client/graphics/GameRenderer.ts | 2 + src/client/graphics/layers/TerritoryLayer.ts | 51 +++------ src/client/graphics/layers/UnitLayer.ts | 103 +++++++++++++++++++ 4 files changed, 123 insertions(+), 42 deletions(-) create mode 100644 src/client/graphics/layers/UnitLayer.ts diff --git a/TODO.txt b/TODO.txt index d17d7f272..679616ecd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -172,11 +172,12 @@ * rewrite EventsDisplay DONE 11/1/2024 * update Mena NPC locations DONE 11/1/2024 * create build menu DONE 11/3/2024 -* add gold -* add troop/worker slider -* add battleship +* add gold DONE 11/4/2024 +* add troop/worker slider DONE 11/4/2024 +* create Unit layer DONE 11/9/2024 +* create Unit interface +* add destroyer * NPC has relations -* fix name rendering * use twitter emojis * private game shows how many players joined * optimize sendBoat function diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 579de4a00..a8c101266 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -14,6 +14,7 @@ import { Leaderboard } from "./layers/Leaderboard"; import { ControlPanel } from "./layers/ControlPanel"; import { UIState } from "./UIState"; import { BuildMenu } from "./layers/radial/BuildMenu"; +import { UnitLayer } from "./layers/UnitLayer"; export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: EventBus, clientID: ClientID): GameRenderer { @@ -61,6 +62,7 @@ export function createRenderer(canvas: HTMLCanvasElement, game: Game, eventBus: const layers: Layer[] = [ new TerrainLayer(game), new TerritoryLayer(game, eventBus), + new UnitLayer(game, eventBus), new NameLayer(game, game.config().theme(), transformHandler, clientID), new UILayer(eventBus, game, clientID, transformHandler), eventsDisplay, diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 85d76d87a..c6dfd6c31 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -1,33 +1,33 @@ -import {PriorityQueue} from "@datastructures-js/priority-queue"; -import {Boat, BoatEvent, Cell, Game, Player, Tile, TileEvent} from "../../../core/game/Game"; -import {PseudoRandom} from "../../../core/PseudoRandom"; -import {Colord} from "colord"; -import {bfs, dist} from "../../../core/Util"; -import {Theme} from "../../../core/configuration/Config"; -import {Layer} from "./Layer"; -import {TransformHandler} from "../TransformHandler"; -import {EventBus} from "../../../core/EventBus"; +import { PriorityQueue } from "@datastructures-js/priority-queue"; +import { Cell, Game, Player, Tile, TileEvent } from "../../../core/game/Game"; +import { PseudoRandom } from "../../../core/PseudoRandom"; +import { Colord } from "colord"; +import { bfs, dist } from "../../../core/Util"; +import { Theme } from "../../../core/configuration/Config"; +import { Layer } from "./Layer"; +import { TransformHandler } from "../TransformHandler"; +import { EventBus } from "../../../core/EventBus"; export class TerritoryLayer implements Layer { private canvas: HTMLCanvasElement private context: CanvasRenderingContext2D private imageData: ImageData - private tileToRenderQueue: PriorityQueue<{tileEvent: TileEvent, lastUpdate: number}> = new PriorityQueue((a, b) => {return a.lastUpdate - b.lastUpdate}) + private tileToRenderQueue: PriorityQueue<{ tileEvent: TileEvent, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate }) private random = new PseudoRandom(123) private theme: Theme = null - private boatToTrail = new Map>() constructor(private game: Game, eventBus: EventBus) { this.theme = game.config().theme() eventBus.on(TileEvent, e => this.tileUpdate(e)) - eventBus.on(BoatEvent, e => this.boatEvent(e)) } + shouldTransform(): boolean { return true } + tick() { } @@ -62,31 +62,6 @@ export class TerritoryLayer implements Layer { ) } - boatEvent(event: BoatEvent) { - if (!this.boatToTrail.has(event.boat)) { - this.boatToTrail.set(event.boat, new Set()) - } - const trail = this.boatToTrail.get(event.boat) - trail.add(event.oldTile) - bfs(event.oldTile, dist(event.oldTile, 3)).forEach(t => { - this.paintTerritory(t) - }) - if (event.boat.isActive()) { - bfs(event.boat.tile(), dist(event.boat.tile(), 4)).forEach( - t => { - if (trail.has(t)) { - this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 150) - } - } - ) - bfs(event.boat.tile(), dist(event.boat.tile(), 2)).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().info()), 255)) - bfs(event.boat.tile(), dist(event.boat.tile(), 1)).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 180)) - } else { - trail.forEach(t => this.paintTerritory(t)) - this.boatToTrail.delete(event.boat) - } - } - renderTerritory() { let numToRender = Math.floor(this.tileToRenderQueue.size() / 10) if (numToRender == 0) { @@ -138,6 +113,6 @@ export class TerritoryLayer implements Layer { } tileUpdate(event: TileEvent) { - this.tileToRenderQueue.push({tileEvent: event, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5)}) + this.tileToRenderQueue.push({ tileEvent: event, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5) }) } } \ No newline at end of file diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts new file mode 100644 index 000000000..a5dbd79db --- /dev/null +++ b/src/client/graphics/layers/UnitLayer.ts @@ -0,0 +1,103 @@ +import { Colord } from "colord"; +import { Theme } from "../../../core/configuration/Config"; +import { Boat, BoatEvent, Cell, Game, Tile } from "../../../core/game/Game"; +import { bfs, dist } from "../../../core/Util"; +import { Layer } from "./Layer"; +import { EventBus } from "../../../core/EventBus"; + +export class UnitLayer implements Layer { + private canvas: HTMLCanvasElement + private context: CanvasRenderingContext2D + private imageData: ImageData + + private boatToTrail = new Map>() + + private theme: Theme = null + + constructor(private game: Game, private eventBus: EventBus) { + this.theme = game.config().theme() + } + + shouldTransform(): boolean { + return true + } + + tick() { + } + + init(game: Game) { + this.canvas = document.createElement('canvas'); + this.context = this.canvas.getContext("2d") + + this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height()) + this.canvas.width = this.game.width(); + this.canvas.height = this.game.height(); + this.context.putImageData(this.imageData, 0, 0); + this.initImageData() + + this.eventBus.on(BoatEvent, e => this.onBoatEvent(e)) + } + + initImageData() { + this.game.forEachTile((tile) => { + const index = (tile.cell().y * this.game.width()) + tile.cell().x + const offset = index * 4 + this.imageData.data[offset + 3] = 0 + }) + } + + renderLayer(context: CanvasRenderingContext2D) { + this.context.putImageData(this.imageData, 0, 0); + context.drawImage( + this.canvas, + -this.game.width() / 2, + -this.game.height() / 2, + this.game.width(), + this.game.height() + ) + } + + + onBoatEvent(event: BoatEvent) { + if (!this.boatToTrail.has(event.boat)) { + this.boatToTrail.set(event.boat, new Set()) + } + const trail = this.boatToTrail.get(event.boat) + trail.add(event.oldTile) + bfs(event.oldTile, dist(event.oldTile, 3)).forEach(t => { + this.clearCell(t.cell()) + }) + if (event.boat.isActive()) { + bfs(event.boat.tile(), dist(event.boat.tile(), 4)).forEach( + t => { + if (trail.has(t)) { + this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 150) + } + } + ) + bfs(event.boat.tile(), dist(event.boat.tile(), 2)).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().info()), 255)) + bfs(event.boat.tile(), dist(event.boat.tile(), 1)).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 180)) + } else { + trail.forEach(t => this.clearCell(t.cell())) + this.boatToTrail.delete(event.boat) + } + } + + + paintCell(cell: Cell, color: Colord, alpha: number) { + const index = (cell.y * this.game.width()) + cell.x + const offset = index * 4 + this.imageData.data[offset] = color.rgba.r; + this.imageData.data[offset + 1] = color.rgba.g; + this.imageData.data[offset + 2] = color.rgba.b; + this.imageData.data[offset + 3] = alpha + } + + clearCell(cell: Cell) { + const index = (cell.y * this.game.width()) + cell.x; + const offset = index * 4; + this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent) + } + + +} \ No newline at end of file From 6a1a09c3351d019617908460341d1b314c718bd1 Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 10 Nov 2024 11:06:09 -0800 Subject: [PATCH 02/25] Create Unit class --- src/client/graphics/layers/UnitLayer.ts | 4 +-- .../graphics/layers/radial/RadialMenu.ts | 4 +-- src/core/execution/BoatAttackExecution.ts | 6 ++-- src/core/game/Game.ts | 23 +++++++------ src/core/game/GameImpl.ts | 34 +++++++++---------- src/core/game/PlayerImpl.ts | 19 ++++++----- src/core/game/{BoatImpl.ts => UnitImpl.ts} | 26 +++++++------- 7 files changed, 61 insertions(+), 55 deletions(-) rename src/core/game/{BoatImpl.ts => UnitImpl.ts} (55%) diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index a5dbd79db..230499652 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -1,6 +1,6 @@ import { Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; -import { Boat, BoatEvent, Cell, Game, Tile } from "../../../core/game/Game"; +import { Unit, BoatEvent, Cell, Game, Tile } from "../../../core/game/Game"; import { bfs, dist } from "../../../core/Util"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; @@ -10,7 +10,7 @@ export class UnitLayer implements Layer { private context: CanvasRenderingContext2D private imageData: ImageData - private boatToTrail = new Map>() + private boatToTrail = new Map>() private theme: Theme = null diff --git a/src/client/graphics/layers/radial/RadialMenu.ts b/src/client/graphics/layers/radial/RadialMenu.ts index 48a8010f1..ab087ab66 100644 --- a/src/client/graphics/layers/radial/RadialMenu.ts +++ b/src/client/graphics/layers/radial/RadialMenu.ts @@ -1,5 +1,5 @@ import { EventBus } from "../../../../core/EventBus"; -import { AllPlayers, Cell, Game, Player } from "../../../../core/game/Game"; +import { AllPlayers, Cell, Game, Player, UnitType } from "../../../../core/game/Game"; import { ClientID } from "../../../../core/Schemas"; import { and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore } from "../../../../core/Util"; import { ContextMenuEvent, MouseUpEvent } from "../../../InputHandler"; @@ -305,7 +305,7 @@ export class RadialMenu implements Layer { if (!tile.isLand()) { return } - if (myPlayer.boats().length >= this.game.config().boatMaxNumber()) { + if (myPlayer.units(UnitType.TransportShip).length >= this.game.config().boatMaxNumber()) { return } diff --git a/src/core/execution/BoatAttackExecution.ts b/src/core/execution/BoatAttackExecution.ts index 23ae6bf4c..c653ee376 100644 --- a/src/core/execution/BoatAttackExecution.ts +++ b/src/core/execution/BoatAttackExecution.ts @@ -1,5 +1,5 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; -import { Boat, Cell, Execution, MutableBoat, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, TileEvent } from "../game/Game"; +import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, TileEvent, UnitType } from "../game/Game"; import { and, bfs, manhattanDistWrapped, sourceDstOceanShore } from "../Util"; import { AttackExecution } from "./AttackExecution"; import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"; @@ -24,7 +24,7 @@ export class BoatAttackExecution implements Execution { private currTileIndex: number = 0 - private boat: MutableBoat + private boat: MutableUnit private aStarPre: AStar private aStarComplete: AStar @@ -48,7 +48,7 @@ export class BoatAttackExecution implements Execution { this.attacker = mg.player(this.attackerID) - if (this.attacker.boats().length >= mg.config().boatMaxNumber()) { + if (this.attacker.units(UnitType.TransportShip).length >= mg.config().boatMaxNumber()) { mg.displayMessage(`No boats available, max ${mg.config().boatMaxNumber()}`, MessageType.WARN, this.attackerID) this.active = false this.attacker.addTroops(this.troops) diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index ddfae6680..63708290c 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -25,6 +25,10 @@ export enum GameMap { Mena } +export enum UnitType { + TransportShip +} + export class Item { constructor(public readonly name: string, public readonly cost: Gold) { } } @@ -145,18 +149,17 @@ export interface Tile { onShore(): boolean } -export interface Boat { +export interface Unit { + type(): UnitType troops(): number tile(): Tile owner(): Player - target(): Player | TerraNullius isActive(): boolean } -export interface MutableBoat extends Boat { +export interface MutableUnit extends Unit { move(tile: Tile): void owner(): MutablePlayer - target(): MutablePlayer | TerraNullius setTroops(troops: number): void delete(): void } @@ -175,7 +178,7 @@ export interface Player { clientID(): ClientID id(): PlayerID type(): PlayerType - boats(): Boat[] + units(...types: UnitType[]): Unit[] ownsTile(cell: Cell): boolean isAlive(): boolean borderTiles(): ReadonlySet @@ -216,14 +219,14 @@ export interface MutablePlayer extends Player { relinquish(tile: Tile): void executions(): Execution[] neighbors(): (MutablePlayer | TerraNullius)[] - boats(): MutableBoat[] + units(...types: UnitType[]): MutableUnit[] incomingAllianceRequests(): MutableAllianceRequest[] outgoingAllianceRequests(): MutableAllianceRequest[] alliances(): MutableAlliance[] allianceWith(other: Player): MutableAlliance | null breakAlliance(alliance: Alliance): void createAllianceRequest(recipient: Player): MutableAllianceRequest - addBoat(troops: number, tile: Tile, target: Player | TerraNullius): MutableBoat + addBoat(troops: number, tile: Tile, target: Player | TerraNullius): MutableUnit target(other: Player): void targets(): MutablePlayer[] transitiveTargets(): MutablePlayer[] @@ -263,7 +266,7 @@ export interface Game { nations(): Nation[] config(): Config displayMessage(message: string, type: MessageType, playerID: PlayerID | null): void - boats(): Boat[] + boats(): Unit[] } export interface MutableGame extends Game { @@ -272,7 +275,7 @@ export interface MutableGame extends Game { players(): MutablePlayer[] addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer executions(): Execution[] - boats(): MutableBoat[] + boats(): MutableUnit[] } export class TileEvent implements GameEvent { @@ -284,7 +287,7 @@ export class PlayerEvent implements GameEvent { } export class BoatEvent implements GameEvent { - constructor(public readonly boat: Boat, public oldTile: Tile) { } + constructor(public readonly boat: Unit, public oldTile: Tile) { } } export class AllianceRequestEvent implements GameEvent { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 117c537ac..45f44517a 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -1,16 +1,16 @@ -import {info} from "console"; -import {Config} from "../configuration/Config"; -import {EventBus} from "../EventBus"; -import {Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Boat, BoatEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation} from "./Game"; -import {TerrainMap} from "./TerrainMapLoader"; -import {PlayerImpl} from "./PlayerImpl"; -import {TerraNulliusImpl} from "./TerraNulliusImpl"; -import {TileImpl} from "./TileImpl"; -import {AllianceRequestImpl} from "./AllianceRequestImpl"; -import {AllianceImpl} from "./AllianceImpl"; -import {ClientID} from "../Schemas"; -import {DisplayMessageEvent, MessageType} from "../../client/graphics/layers/EventsDisplay"; -import {BoatImpl} from "./BoatImpl"; +import { info } from "console"; +import { Config } from "../configuration/Config"; +import { EventBus } from "../EventBus"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, BoatEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation } from "./Game"; +import { TerrainMap } from "./TerrainMapLoader"; +import { PlayerImpl } from "./PlayerImpl"; +import { TerraNulliusImpl } from "./TerraNulliusImpl"; +import { TileImpl } from "./TileImpl"; +import { AllianceRequestImpl } from "./AllianceRequestImpl"; +import { AllianceImpl } from "./AllianceImpl"; +import { ClientID } from "../Schemas"; +import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"; +import { UnitImpl } from "./UnitImpl"; export function createGame(terrainMap: TerrainMap, eventBus: EventBus, config: Config): Game { return new GameImpl(terrainMap, eventBus, config) @@ -58,8 +58,8 @@ export class GameImpl implements MutableGame { n.strength )) } - boats(): BoatImpl[] { - return Array.from(this._players.values()).flatMap(p => p._boats) + boats(): UnitImpl[] { + return Array.from(this._players.values()).flatMap(p => p._units) } nations(): Nation[] { return this.nations_ @@ -354,8 +354,8 @@ export class GameImpl implements MutableGame { return false } - public fireBoatUpdateEvent(boat: Boat, oldTile: Tile) { - this.eventBus.emit(new BoatEvent(boat, oldTile)) + public fireUnitUpdateEvent(boat: Unit, oldTile: Tile) { + this.eventBus.emit(new UnitEvent(boat, oldTile)) } public breakAlliance(breaker: Player, alliance: Alliance) { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index eb3b0b03c..0079aa72d 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -1,8 +1,8 @@ -import { MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent, EmojiMessage, EmojiMessageEvent, AllPlayers, Gold } from "./Game"; +import { MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent, EmojiMessage, EmojiMessageEvent, AllPlayers, Gold, UnitType } from "./Game"; import { ClientID } from "../Schemas"; import { processName, simpleHash } from "../Util"; import { CellString, GameImpl } from "./GameImpl"; -import { BoatImpl } from "./BoatImpl"; +import { UnitImpl } from "./UnitImpl"; import { TileImpl } from "./TileImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { MessageType } from "../../client/graphics/layers/EventsDisplay"; @@ -29,7 +29,7 @@ export class PlayerImpl implements MutablePlayer { public _borderTiles: Set = new Set(); - public _boats: BoatImpl[] = []; + public _units: UnitImpl[] = []; public _tiles: Map = new Map(); private _name: string; @@ -73,15 +73,16 @@ export class PlayerImpl implements MutablePlayer { } - addBoat(troops: number, tile: Tile, target: Player | TerraNullius): BoatImpl { - const b = new BoatImpl(this.gs, tile, troops, this, target as PlayerImpl | TerraNulliusImpl); - this._boats.push(b); - this.gs.fireBoatUpdateEvent(b, b.tile()); + addBoat(troops: number, tile: Tile): UnitImpl { + const b = new UnitImpl(UnitType.TransportShip, this.gs, tile, troops, this); + this._units.push(b); + this.gs.fireUnitUpdateEvent(b, b.tile()); return b; } - boats(): BoatImpl[] { - return this._boats; + units(...types: UnitType[]): UnitImpl[] { + const ts = new Set(types) + return this._units.filter(u => ts.has(u.type())); } sharesBorderWith(other: Player | TerraNullius): boolean { diff --git a/src/core/game/BoatImpl.ts b/src/core/game/UnitImpl.ts similarity index 55% rename from src/core/game/BoatImpl.ts rename to src/core/game/UnitImpl.ts index fe13cc3c6..249c30ea3 100644 --- a/src/core/game/BoatImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -1,24 +1,28 @@ -import {MutableBoat, Tile, TerraNullius} from "./Game"; -import {GameImpl} from "./GameImpl"; -import {PlayerImpl} from "./PlayerImpl"; -import {TerraNulliusImpl} from "./TerraNulliusImpl"; +import { MutableUnit, Tile, TerraNullius, UnitType } from "./Game"; +import { GameImpl } from "./GameImpl"; +import { PlayerImpl } from "./PlayerImpl"; +import { TerraNulliusImpl } from "./TerraNulliusImpl"; -export class BoatImpl implements MutableBoat { +export class UnitImpl implements MutableUnit { private _active = true; constructor( + private _type: UnitType, private g: GameImpl, private _tile: Tile, private _troops: number, private _owner: PlayerImpl, - private _target: PlayerImpl | TerraNulliusImpl ) { } + type(): UnitType { + return this._type + } + move(tile: Tile): void { const oldTile = this._tile; this._tile = tile; - this.g.fireBoatUpdateEvent(this, oldTile); + this.g.fireUnitUpdateEvent(this, oldTile); } setTroops(troops: number): void { this._troops = troops; @@ -32,13 +36,11 @@ export class BoatImpl implements MutableBoat { owner(): PlayerImpl { return this._owner; } - target(): PlayerImpl | TerraNullius { - return this._target; - } + delete(): void { - this._owner._boats = this._owner._boats.filter(b => b != this); + this._owner._units = this._owner._units.filter(b => b != this); this._active = false; - this.g.fireBoatUpdateEvent(this, this._tile); + this.g.fireUnitUpdateEvent(this, this._tile); } isActive(): boolean { return this._active; From c7951d77c0119dfe940e6fcbfd2cbd2f0e636cef Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 10 Nov 2024 18:28:21 -0800 Subject: [PATCH 03/25] adding destroyer --- TODO.txt | 2 +- resources/images/Destroyer.svg | 47 +++++++++++++++ resources/images/DestroyerIconWhite.svg | 59 +++++++++++++++++++ src/client/ClientGame.ts | 2 +- src/client/Transport.ts | 21 ++++++- src/client/graphics/layers/UnitLayer.ts | 43 ++++++++++---- .../graphics/layers/radial/BuildMenu.ts | 15 ++++- src/core/Schemas.ts | 18 ++++-- src/core/execution/DestroyerExecution.ts | 39 ++++++++++++ src/core/execution/ExecutionManager.ts | 4 +- src/core/execution/FakeHumanExecution.ts | 4 +- src/core/execution/NukeExecution.ts | 4 +- ...Execution.ts => TransportShipExecution.ts} | 8 ++- src/core/game/Game.ts | 15 +++-- src/core/game/GameImpl.ts | 6 +- src/core/game/PlayerImpl.ts | 4 +- 16 files changed, 249 insertions(+), 42 deletions(-) create mode 100644 resources/images/Destroyer.svg create mode 100644 resources/images/DestroyerIconWhite.svg create mode 100644 src/core/execution/DestroyerExecution.ts rename src/core/execution/{BoatAttackExecution.ts => TransportShipExecution.ts} (95%) diff --git a/TODO.txt b/TODO.txt index 679616ecd..6faccdcdf 100644 --- a/TODO.txt +++ b/TODO.txt @@ -175,7 +175,7 @@ * add gold DONE 11/4/2024 * add troop/worker slider DONE 11/4/2024 * create Unit layer DONE 11/9/2024 -* create Unit interface +* create Unit interface DONE 11/10/2024 * add destroyer * NPC has relations * use twitter emojis diff --git a/resources/images/Destroyer.svg b/resources/images/Destroyer.svg new file mode 100644 index 000000000..bebcde38f --- /dev/null +++ b/resources/images/Destroyer.svg @@ -0,0 +1,47 @@ + + + + + + diff --git a/resources/images/DestroyerIconWhite.svg b/resources/images/DestroyerIconWhite.svg new file mode 100644 index 000000000..6bad21f6a --- /dev/null +++ b/resources/images/DestroyerIconWhite.svg @@ -0,0 +1,59 @@ + + + + + + + + + diff --git a/src/client/ClientGame.ts b/src/client/ClientGame.ts index d4663178a..d307749be 100644 --- a/src/client/ClientGame.ts +++ b/src/client/ClientGame.ts @@ -1,5 +1,5 @@ import { Executor } from "../core/execution/ExecutionManager"; -import { Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, BoatEvent, Tile, PlayerType, GameMap, Difficulty } from "../core/game/Game"; +import { Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Player, Game, UnitEvent, Tile, PlayerType, GameMap, Difficulty } from "../core/game/Game"; import { createGame } from "../core/game/GameImpl"; import { EventBus } from "../core/EventBus"; import { Config, getConfig } from "../core/configuration/Config"; diff --git a/src/client/Transport.ts b/src/client/Transport.ts index fe969c6af..0098b54d6 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -1,7 +1,7 @@ import { Config } from "../core/configuration/Config" import { EventBus, GameEvent } from "../core/EventBus" -import { AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType } from "../core/game/Game" -import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema } from "../core/Schemas" +import { AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType, Tile } from "../core/game/Game" +import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, CreateDestroyerIntent, GameID, Intent, ServerMessage, ServerMessageSchema } from "../core/Schemas" import { LocalServer } from "./LocalServer" @@ -47,6 +47,12 @@ export class SendBoatAttackIntentEvent implements GameEvent { ) { } } +export class SendCreateDestroyerIntentEvent implements GameEvent { + constructor( + public readonly cell: Cell, + ) { } +} + export class SendTargetPlayerIntentEvent implements GameEvent { constructor( public readonly targetID: PlayerID, @@ -115,6 +121,7 @@ export class Transport { this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e)) this.eventBus.on(SendNukeIntentEvent, (e) => this.onSendNukeIntent(e)) this.eventBus.on(SendSetTargetTroopRatioEvent, (e) => this.onSendSetTargetTroopRatioEvent(e)) + this.eventBus.on(SendCreateDestroyerIntentEvent, (e) => this.onCreateDestroyerIntent(e)) } connect(onconnect: () => void, onmessage: (message: ServerMessage) => void) { @@ -314,6 +321,16 @@ export class Transport { }) } + private onCreateDestroyerIntent(event: SendCreateDestroyerIntentEvent) { + this.sendIntent({ + type: "create_destroyer", + clientID: this.clientID, + player: this.playerID, + x: event.cell.x, + y: event.cell.y, + }) + } + private sendIntent(intent: Intent) { if (this.isLocal || this.socket.readyState === WebSocket.OPEN) { const msg = ClientIntentMessageSchema.parse({ diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index 230499652..ae0be9338 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -1,6 +1,6 @@ import { Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; -import { Unit, BoatEvent, Cell, Game, Tile } from "../../../core/game/Game"; +import { Unit, UnitEvent, Cell, Game, Tile, UnitType } from "../../../core/game/Game"; import { bfs, dist } from "../../../core/Util"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; @@ -35,7 +35,7 @@ export class UnitLayer implements Layer { this.context.putImageData(this.imageData, 0, 0); this.initImageData() - this.eventBus.on(BoatEvent, e => this.onBoatEvent(e)) + this.eventBus.on(UnitEvent, e => this.onUnitEvent(e)) } initImageData() { @@ -58,28 +58,47 @@ export class UnitLayer implements Layer { } - onBoatEvent(event: BoatEvent) { - if (!this.boatToTrail.has(event.boat)) { - this.boatToTrail.set(event.boat, new Set()) + onUnitEvent(event: UnitEvent) { + switch (event.unit.type()) { + case UnitType.TransportShip: + this.handleBoatEvent(event) + break + case UnitType.Destroyer: + this.handleDestroyerEvent(event) + break + default: + throw Error(`event for unit ${event.unit.type()} not supported`) } - const trail = this.boatToTrail.get(event.boat) + } + + private handleDestroyerEvent(event: UnitEvent) { + + } + + private handleBoatEvent(event: UnitEvent) { + if (!this.boatToTrail.has(event.unit)) { + this.boatToTrail.set(event.unit, new Set()) + } + const trail = this.boatToTrail.get(event.unit) trail.add(event.oldTile) bfs(event.oldTile, dist(event.oldTile, 3)).forEach(t => { this.clearCell(t.cell()) }) - if (event.boat.isActive()) { - bfs(event.boat.tile(), dist(event.boat.tile(), 4)).forEach( + 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.boat.owner().info()), 150) + this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 150) } } ) - bfs(event.boat.tile(), dist(event.boat.tile(), 2)).forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.boat.owner().info()), 255)) - bfs(event.boat.tile(), dist(event.boat.tile(), 1)).forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.boat.owner().info()), 180)) + 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)) + .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 180)) } else { trail.forEach(t => this.clearCell(t.cell())) - this.boatToTrail.delete(event.boat) + this.boatToTrail.delete(event.unit) } } diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 5ff17f655..21fb6e44f 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -2,8 +2,9 @@ import { LitElement, html, css } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { EventBus } from '../../../../core/EventBus'; import { Cell, Game, Item, Items, Player } from '../../../../core/game/Game'; -import { SendNukeIntentEvent } from '../../../Transport'; +import { SendCreateDestroyerIntentEvent, SendNukeIntentEvent } from '../../../Transport'; import nukeIcon from '../../../../../resources/images/NukeIconWhite.svg'; +import destroyerIcon from '../../../../../resources/images/DestroyerIconWhite.svg'; import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg'; import { renderNumber } from '../../Utils'; import { ContextMenuEvent } from '../../../InputHandler'; @@ -16,6 +17,7 @@ interface BuildItem { const buildTable: BuildItem[][] = [ [ { item: Items.Nuke, icon: nukeIcon }, + { item: Items.Destroyer, icon: destroyerIcon }, // { id: 'battleship', name: 'Battleship', icon: '🚢', cost: 500, buildTime: 20 } ] ]; @@ -146,8 +148,15 @@ export class BuildMenu extends LitElement { return this.myPlayer && this.myPlayer.gold() >= item.item.cost; } - public onBuildSelected: (item: BuildItem) => void = () => { - this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null)) + public onBuildSelected = (item: BuildItem) => { + switch (item.item.name) { + case "Nuke": + this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null)) + break + case "Destroyer": + this.eventBus.emit(new SendCreateDestroyerIntentEvent(this.clickedCell)) + + } this.hideMenu() }; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 47f96574c..8d4b1be04 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -15,6 +15,7 @@ export type Intent = SpawnIntent | DonateIntent | NukeIntent | TargetTroopRatioIntent + | CreateDestroyerIntent export type AttackIntent = z.infer export type SpawnIntent = z.infer @@ -26,7 +27,8 @@ export type TargetPlayerIntent = z.infer export type EmojiIntent = z.infer export type DonateIntent = z.infer export type NukeIntent = z.infer -export type TargetTroopRatioIntent = z.infer +export type TargetTroopRatioIntent = z.infer +export type CreateDestroyerIntent = z.infer export type Turn = z.infer export type GameConfig = z.infer @@ -67,7 +69,7 @@ const EmojiSchema = z.string().refine( ); // Zod schemas const BaseIntentSchema = z.object({ - type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'nuke', 'troop_ratio']), + type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'nuke', 'troop_ratio', 'create_destroyer']), clientID: z.string(), }); @@ -153,12 +155,19 @@ export const NukeIntentSchema = BaseIntentSchema.extend({ magnitude: z.number().nullable(), }) -export const TargetTroopRatioSchema = BaseIntentSchema.extend({ +export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({ type: z.literal('troop_ratio'), player: z.string(), ratio: z.number().min(0).max(1), }) +export const CreateDestroyerIntentSchema = BaseIntentSchema.extend({ + type: z.literal('create_destroyer'), + player: z.string(), + x: z.number(), + y: z.number(), +}) + const IntentSchema = z.union([ AttackIntentSchema, SpawnIntentSchema, @@ -170,7 +179,8 @@ const IntentSchema = z.union([ EmojiIntentSchema, DonateIntentSchema, NukeIntentSchema, - TargetTroopRatioSchema, + TargetTroopRatioIntentSchema, + CreateDestroyerIntentSchema, ]); const TurnSchema = z.object({ diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts new file mode 100644 index 000000000..621dbdca7 --- /dev/null +++ b/src/core/execution/DestroyerExecution.ts @@ -0,0 +1,39 @@ +import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, UnitType } from "../game/Game"; + +export class DestroyerExecution implements Execution { + + private _owner: MutablePlayer + private active = true + private destroyer: MutableUnit = null + private mg: MutableGame = null + + constructor( + private playerID: PlayerID, + private cell: Cell, + ) { } + + + init(mg: MutableGame, ticks: number): void { + this._owner = mg.player(this.playerID) + this.mg = mg + } + + tick(ticks: number): void { + if (this.destroyer == null) { + this.destroyer = this._owner.addUnit(UnitType.Destroyer, 0, this.mg.tile(this.cell)) + } + } + + owner(): MutablePlayer { + return null + } + + isActive(): boolean { + return this.active + } + + activeDuringSpawnPhase(): boolean { + return false + } + +} \ No newline at end of file diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 44cc61094..d3a272ee2 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -3,7 +3,7 @@ import { AttackIntent, BoatAttackIntentSchema, GameID, Intent, Turn } from "../S import { AttackExecution } from "./AttackExecution"; import { SpawnExecution } from "./SpawnExecution"; import { BotSpawner } from "./BotSpawner"; -import { BoatAttackExecution } from "./BoatAttackExecution"; +import { TransportShipExecution } from "./TransportShipExecution"; import { PseudoRandom } from "../PseudoRandom"; import { FakeHumanExecution } from "./FakeHumanExecution"; import Usernames from '../../../resources/Usernames.txt' @@ -52,7 +52,7 @@ export class Executor { new Cell(intent.x, intent.y) ) } else if (intent.type == "boat") { - return new BoatAttackExecution( + return new TransportShipExecution( intent.attackerID, intent.targetID, new Cell(intent.x, intent.y), diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts index 33be8a15e..3cb042824 100644 --- a/src/core/execution/FakeHumanExecution.ts +++ b/src/core/execution/FakeHumanExecution.ts @@ -2,7 +2,7 @@ import { Cell, Execution, MutableGame, MutablePlayer, Player, PlayerInfo, Player import { PseudoRandom } from "../PseudoRandom" import { and, bfs, dist, simpleHash } from "../Util"; import { AttackExecution } from "./AttackExecution"; -import { BoatAttackExecution } from "./BoatAttackExecution"; +import { TransportShipExecution } from "./TransportShipExecution"; import { SpawnExecution } from "./SpawnExecution"; export class FakeHumanExecution implements Execution { @@ -194,7 +194,7 @@ export class FakeHumanExecution implements Execution { continue } - this.mg.addExecution(new BoatAttackExecution( + this.mg.addExecution(new TransportShipExecution( this.player.id(), dst.hasOwner() ? dst.owner().id() : null, dst.cell(), diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 7b004159a..8119101bc 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -48,7 +48,9 @@ export class NukeExecution implements Execution { mp.removeTroops(mp.troops() / mp.numTilesOwned()) } } - this.mg.boats().filter(b => euclideanDist(this.cell, b.tile().cell()) < this.magnitude + 50).forEach(b => b.delete()) + this.mg.units() + .filter(b => euclideanDist(this.cell, b.tile().cell()) < this.magnitude + 50) + .forEach(b => b.delete()) this.active = false } diff --git a/src/core/execution/BoatAttackExecution.ts b/src/core/execution/TransportShipExecution.ts similarity index 95% rename from src/core/execution/BoatAttackExecution.ts rename to src/core/execution/TransportShipExecution.ts index c653ee376..32c3b8320 100644 --- a/src/core/execution/BoatAttackExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -4,7 +4,7 @@ import { and, bfs, manhattanDistWrapped, sourceDstOceanShore } from "../Util"; import { AttackExecution } from "./AttackExecution"; import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"; -export class BoatAttackExecution implements Execution { +export class TransportShipExecution implements Execution { private lastMove: number @@ -86,7 +86,7 @@ export class BoatAttackExecution implements Execution { this.aStarPre.compute(5) this.path = this.aStarPre.reconstructPath() if (this.path != null) { - this.boat = this.attacker.addBoat(this.troops, this.src, this.target) + this.boat = this.attacker.addUnit(UnitType.TransportShip, this.troops, this.src) } else { console.log('got null path') this.active = false @@ -126,7 +126,9 @@ export class BoatAttackExecution implements Execution { 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.mg.addExecution( + new AttackExecution(this.troops, this.attacker.id(), this.targetID, this.dst.cell(), null, false) + ) } this.boat.delete() this.active = false diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 63708290c..19a773b12 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -26,7 +26,8 @@ export enum GameMap { } export enum UnitType { - TransportShip + TransportShip, + Destroyer } export class Item { @@ -35,6 +36,7 @@ export class Item { export const Items = { Nuke: new Item("Nuke", 1_000_000), + Destroyer: new Item("Destroyer", 100_000) } as const; export class Nation { @@ -226,7 +228,6 @@ export interface MutablePlayer extends Player { allianceWith(other: Player): MutableAlliance | null breakAlliance(alliance: Alliance): void createAllianceRequest(recipient: Player): MutableAllianceRequest - addBoat(troops: number, tile: Tile, target: Player | TerraNullius): MutableUnit target(other: Player): void targets(): MutablePlayer[] transitiveTargets(): MutablePlayer[] @@ -242,6 +243,8 @@ export interface MutablePlayer extends Player { setTroops(troops: number): void addTroops(troops: number): void removeTroops(troops: number): number + + addUnit(type: UnitType, troops: number, tile: Tile): MutableUnit } export interface Game { @@ -266,7 +269,7 @@ export interface Game { nations(): Nation[] config(): Config displayMessage(message: string, type: MessageType, playerID: PlayerID | null): void - boats(): Unit[] + units(...types: UnitType[]): Unit[] } export interface MutableGame extends Game { @@ -275,7 +278,7 @@ export interface MutableGame extends Game { players(): MutablePlayer[] addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer executions(): Execution[] - boats(): MutableUnit[] + units(...types: UnitType[]): MutableUnit[] } export class TileEvent implements GameEvent { @@ -286,8 +289,8 @@ export class PlayerEvent implements GameEvent { constructor(public readonly player: Player) { } } -export class BoatEvent implements GameEvent { - constructor(public readonly boat: Unit, public oldTile: Tile) { } +export class UnitEvent implements GameEvent { + constructor(public readonly unit: Unit, public oldTile: Tile) { } } export class AllianceRequestEvent implements GameEvent { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 45f44517a..2e3dce159 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -1,7 +1,7 @@ import { info } from "console"; import { Config } from "../configuration/Config"; import { EventBus } from "../EventBus"; -import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, BoatEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation } from "./Game"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType } from "./Game"; import { TerrainMap } from "./TerrainMapLoader"; import { PlayerImpl } from "./PlayerImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; @@ -58,8 +58,8 @@ export class GameImpl implements MutableGame { n.strength )) } - boats(): UnitImpl[] { - return Array.from(this._players.values()).flatMap(p => p._units) + units(...types: UnitType[]): UnitImpl[] { + return Array.from(this._players.values()).flatMap(p => p.units(...types)) } nations(): Nation[] { return this.nations_ diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 0079aa72d..7e7489df0 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -73,8 +73,8 @@ export class PlayerImpl implements MutablePlayer { } - addBoat(troops: number, tile: Tile): UnitImpl { - const b = new UnitImpl(UnitType.TransportShip, this.gs, tile, troops, this); + addUnit(type: UnitType, troops: number, tile: Tile): UnitImpl { + const b = new UnitImpl(type, this.gs, tile, troops, this); this._units.push(b); this.gs.fireUnitUpdateEvent(b, b.tile()); return b; From 975750b2942a8ef6e4544afc5b265ade9b539cdc Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 10 Nov 2024 19:50:57 -0800 Subject: [PATCH 04/25] destroyer moves upwards --- src/client/graphics/layers/UnitLayer.ts | 10 ++- src/core/Util.ts | 4 ++ src/core/execution/DestroyerExecution.ts | 2 + src/core/execution/ExecutionManager.ts | 90 +++++++++++++----------- src/core/game/Game.ts | 2 +- 5 files changed, 64 insertions(+), 44 deletions(-) diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index ae0be9338..c2faf248e 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -1,7 +1,7 @@ import { Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; import { Unit, UnitEvent, Cell, Game, Tile, UnitType } from "../../../core/game/Game"; -import { bfs, dist } from "../../../core/Util"; +import { bfs, dist, euclDist } from "../../../core/Util"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; @@ -72,7 +72,13 @@ export class UnitLayer implements Layer { } private handleDestroyerEvent(event: UnitEvent) { - + bfs(event.oldTile, euclDist(event.oldTile, 3)).forEach(t => { + this.clearCell(t.cell()) + }) + bfs(event.unit.tile(), euclDist(event.unit.tile(), 3)) + .forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255)) + bfs(event.unit.tile(), euclDist(event.unit.tile(), 2)) + .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 180)) } private handleBoatEvent(event: UnitEvent) { diff --git a/src/core/Util.ts b/src/core/Util.ts index 0f60c874e..f4e500ab7 100644 --- a/src/core/Util.ts +++ b/src/core/Util.ts @@ -30,6 +30,10 @@ export function within(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } +export function euclDist(root: Tile, dist: number): (tile: Tile) => boolean { + return (n: Tile) => euclideanDist(root.cell(), n.cell()) <= dist; +} + export function dist(root: Tile, dist: number): (tile: Tile) => boolean { return (n: Tile) => manhattanDist(root.cell(), n.cell()) <= dist; } diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index 621dbdca7..22267293a 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -21,7 +21,9 @@ export class DestroyerExecution implements Execution { tick(ticks: number): void { if (this.destroyer == null) { this.destroyer = this._owner.addUnit(UnitType.Destroyer, 0, this.mg.tile(this.cell)) + return } + this.destroyer.move(this.destroyer.tile().neighbors()[0]) } owner(): MutablePlayer { diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index d3a272ee2..49575021e 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -16,6 +16,7 @@ import { EmojiExecution } from "./EmojiExecution"; import { DonateExecution } from "./DonateExecution"; import { NukeExecution } from "./NukeExecution"; import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution"; +import { DestroyerExecution } from "./DestroyerExecution"; @@ -36,50 +37,57 @@ export class Executor { } createExec(intent: Intent): Execution { - if (intent.type == "attack") { - const source: Cell | null = intent.sourceX != null && intent.sourceY != null ? new Cell(intent.sourceX, intent.sourceY) : null - const target: Cell | null = intent.targetX != null && intent.targetY != null ? new Cell(intent.targetX, intent.targetY) : null - return new AttackExecution( - intent.troops, - intent.attackerID, - intent.targetID, - source, - target, - ) - } else if (intent.type == "spawn") { - return new SpawnExecution( - new PlayerInfo(sanitize(intent.name), intent.playerType, intent.clientID, intent.playerID), - new Cell(intent.x, intent.y) - ) - } else if (intent.type == "boat") { - return new TransportShipExecution( - intent.attackerID, - intent.targetID, - new Cell(intent.x, intent.y), - intent.troops - ) - } else if (intent.type == "allianceRequest") { - return new AllianceRequestExecution(intent.requestor, intent.recipient) - } else if (intent.type == "allianceRequestReply") { - return new AllianceRequestReplyExecution(intent.requestor, intent.recipient, intent.accept) - } else if (intent.type == "breakAlliance") { - return new BreakAllianceExecution(intent.requestor, intent.recipient) - } else if (intent.type == "targetPlayer") { - return new TargetPlayerExecution(intent.requestor, intent.target) - } else if (intent.type == "emoji") { - return new EmojiExecution(intent.sender, intent.recipient, intent.emoji) - } else if (intent.type == "donate") { - return new DonateExecution(intent.sender, intent.recipient, intent.troops) - } else if (intent.type == "nuke") { - return new NukeExecution(intent.sender, new Cell(intent.x, intent.y), intent.magnitude) - } else if (intent.type == "troop_ratio") { - return new SetTargetTroopRatioExecution(intent.player, intent.ratio) - } else { - throw new Error(`intent type ${intent} not found`) + switch (intent.type) { + case "attack": { + const source: Cell | null = intent.sourceX != null && intent.sourceY != null + ? new Cell(intent.sourceX, intent.sourceY) + : null; + const target: Cell | null = intent.targetX != null && intent.targetY != null + ? new Cell(intent.targetX, intent.targetY) + : null; + return new AttackExecution( + intent.troops, + intent.attackerID, + intent.targetID, + source, + target, + ); + } + case "spawn": + return new SpawnExecution( + new PlayerInfo(sanitize(intent.name), intent.playerType, intent.clientID, intent.playerID), + new Cell(intent.x, intent.y) + ); + case "boat": + return new TransportShipExecution( + intent.attackerID, + intent.targetID, + new Cell(intent.x, intent.y), + intent.troops + ); + case "allianceRequest": + return new AllianceRequestExecution(intent.requestor, intent.recipient); + case "allianceRequestReply": + return new AllianceRequestReplyExecution(intent.requestor, intent.recipient, intent.accept); + case "breakAlliance": + return new BreakAllianceExecution(intent.requestor, intent.recipient); + case "targetPlayer": + return new TargetPlayerExecution(intent.requestor, intent.target); + case "emoji": + return new EmojiExecution(intent.sender, intent.recipient, intent.emoji); + case "donate": + return new DonateExecution(intent.sender, intent.recipient, intent.troops); + case "nuke": + return new NukeExecution(intent.sender, new Cell(intent.x, intent.y), intent.magnitude); + case "troop_ratio": + return new SetTargetTroopRatioExecution(intent.player, intent.ratio); + case "create_destroyer": + return new DestroyerExecution(intent.player, new Cell(intent.x, intent.y)) + default: + throw new Error(`intent type ${intent} not found`); } } - spawnBots(numBots: number): Execution[] { return new BotSpawner(this.gs, this.gameID).spawnBots(numBots).map(i => this.createExec(i)) } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 19a773b12..7a20e73cd 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -36,7 +36,7 @@ export class Item { export const Items = { Nuke: new Item("Nuke", 1_000_000), - Destroyer: new Item("Destroyer", 100_000) + Destroyer: new Item("Destroyer", 10) } as const; export class Nation { From f742f8acbfd2b41bb4284353f84543ccfe68e1a9 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 11 Nov 2024 07:09:23 -0800 Subject: [PATCH 05/25] working on pathfinding --- src/core/PathFinding.ts | 71 ++++++++++++++++++++ src/core/execution/DestroyerExecution.ts | 2 +- src/core/execution/TransportShipExecution.ts | 69 +------------------ 3 files changed, 73 insertions(+), 69 deletions(-) create mode 100644 src/core/PathFinding.ts diff --git a/src/core/PathFinding.ts b/src/core/PathFinding.ts new file mode 100644 index 000000000..0cfc2b8ba --- /dev/null +++ b/src/core/PathFinding.ts @@ -0,0 +1,71 @@ +import { PriorityQueue } from "@datastructures-js/priority-queue"; +import { Tile } from "./game/Game"; + + +export class AStar { + private openSet: PriorityQueue<{ tile: Tile; fScore: number; }>; + private cameFrom: Map; + private gScore: Map; + private current: Tile | null; + public completed: boolean; + + constructor(private src: Tile, private dst: Tile) { + this.openSet = new PriorityQueue<{ tile: Tile; fScore: number; }>( + (a, b) => a.fScore - b.fScore + ); + this.cameFrom = new Map(); + this.gScore = new Map(); + this.current = null; + this.completed = false; + + this.gScore.set(src, 0); + this.openSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) }); + } + + compute(iterations: number): boolean { + if (this.completed) return true; + + while (!this.openSet.isEmpty()) { + iterations--; + this.current = this.openSet.dequeue()!.tile; + if (iterations <= 0) { + return false; + } + + if (this.current === this.dst) { + this.completed = true; + return true; + } + + for (const neighbor of this.current.neighborsWrapped()) { + if (neighbor != this.dst && neighbor.isLand()) continue; // Skip non-water tiles + + const tentativeGScore = this.gScore.get(this.current)! + 100 - neighbor.magnitude(); + + if (!this.gScore.has(neighbor) || tentativeGScore < this.gScore.get(neighbor)!) { + this.cameFrom.set(neighbor, this.current); + this.gScore.set(neighbor, tentativeGScore); + const fScore = tentativeGScore + this.heuristic(neighbor, this.dst); + + this.openSet.enqueue({ tile: neighbor, fScore: fScore }); + } + } + } + + return this.completed; + } + + private heuristic(a: Tile, b: Tile): number { + // Manhattan distance + return Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y); + } + + public reconstructPath(): Tile[] { + const path = [this.current!]; + while (this.cameFrom.has(this.current!)) { + this.current = this.cameFrom.get(this.current!)!; + path.unshift(this.current); + } + return path; + } +} diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index 22267293a..d25ddfd8e 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -1,4 +1,4 @@ -import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, UnitType } from "../game/Game"; export class DestroyerExecution implements Execution { diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 32c3b8320..94dc04a5b 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -1,8 +1,8 @@ -import { PriorityQueue } from "@datastructures-js/priority-queue"; import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, PlayerID, TerraNullius, Tile, TileEvent, UnitType } from "../game/Game"; import { and, bfs, manhattanDistWrapped, sourceDstOceanShore } from "../Util"; import { AttackExecution } from "./AttackExecution"; import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"; +import { AStar } from "../PathFinding"; export class TransportShipExecution implements Execution { @@ -150,70 +150,3 @@ export class TransportShipExecution implements Execution { } -export class AStar { - private openSet: PriorityQueue<{ tile: Tile, fScore: number }>; - private cameFrom: Map; - private gScore: Map; - private current: Tile | null; - public completed: boolean; - - constructor(private src: Tile, private dst: Tile) { - this.openSet = new PriorityQueue<{ tile: Tile, fScore: number }>( - (a, b) => a.fScore - b.fScore - ); - this.cameFrom = new Map(); - this.gScore = new Map(); - this.current = null; - this.completed = false; - - this.gScore.set(src, 0); - this.openSet.enqueue({ tile: src, fScore: this.heuristic(src, dst) }); - } - - compute(iterations: number): boolean { - if (this.completed) return true; - - while (!this.openSet.isEmpty()) { - iterations-- - this.current = this.openSet.dequeue()!.tile; - if (iterations <= 0) { - return false - } - - if (this.current === this.dst) { - this.completed = true; - return true; - } - - for (const neighbor of this.current.neighborsWrapped()) { - if (neighbor != this.dst && neighbor.isLand()) continue; // Skip non-water tiles - - const tentativeGScore = this.gScore.get(this.current)! + 100 - neighbor.magnitude(); - - if (!this.gScore.has(neighbor) || tentativeGScore < this.gScore.get(neighbor)!) { - this.cameFrom.set(neighbor, this.current); - this.gScore.set(neighbor, tentativeGScore); - const fScore = tentativeGScore + this.heuristic(neighbor, this.dst); - - this.openSet.enqueue({ tile: neighbor, fScore: fScore }); - } - } - } - - return this.completed; - } - - private heuristic(a: Tile, b: Tile): number { - // Manhattan distance - return Math.abs(a.cell().x - b.cell().x) + Math.abs(a.cell().y - b.cell().y); - } - - public reconstructPath(): Tile[] { - const path = [this.current!]; - while (this.cameFrom.has(this.current!)) { - this.current = this.cameFrom.get(this.current!)!; - path.unshift(this.current); - } - return path; - } -} \ No newline at end of file From f0db9324d7338cebc7874b2e97b0d7cbb8b06767 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Mon, 11 Nov 2024 16:36:49 -0800 Subject: [PATCH 06/25] working on destroyer --- src/core/PathFinding.ts | 30 +++++++++++++++++++++ src/core/execution/DestroyerExecution.ts | 33 ++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/core/PathFinding.ts b/src/core/PathFinding.ts index 0cfc2b8ba..7db19b4ff 100644 --- a/src/core/PathFinding.ts +++ b/src/core/PathFinding.ts @@ -69,3 +69,33 @@ export class AStar { return path; } } + +export class PathFinder { + + private curr: Tile = null + private dst: Tile = null + private path: Tile[] + private aStar: AStar + + constructor() { + + } + + nextTile(curr: Tile, dst: Tile): Tile { + if (curr != this.curr || dst != this.dst || this.path == null) { + this.curr = curr + this.dst = dst + this.path = null + this.aStar = new AStar(curr, dst) + if (this.aStar.compute(1000)) { + this.path = this.aStar.reconstructPath() + } else { + return null + } + } + if (this.path.length == 0) { + return null + } + return this.path.shift() + } +} diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index d25ddfd8e..3b5233da8 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -1,4 +1,6 @@ -import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, UnitType } from "../game/Game"; +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; +import { AStar, PathFinder } from "../PathFinding"; +import { manhattanDist } from "../Util"; export class DestroyerExecution implements Execution { @@ -7,6 +9,12 @@ export class DestroyerExecution implements Execution { private destroyer: MutableUnit = null private mg: MutableGame = null + private target: MutableUnit = null + private pathfinder = new PathFinder() + + // TODO: put in config + private searchRange = 100 + constructor( private playerID: PlayerID, private cell: Cell, @@ -23,7 +31,28 @@ export class DestroyerExecution implements Execution { this.destroyer = this._owner.addUnit(UnitType.Destroyer, 0, this.mg.tile(this.cell)) return } - this.destroyer.move(this.destroyer.tile().neighbors()[0]) + if (!this.destroyer.isActive()) { + this.active = false + return + } + if (this.target == null) { + const ships = this.mg.units(UnitType.TransportShip) + .filter(u => manhattanDist(u.tile().cell(), this.destroyer.tile().cell())) + .filter(u => u.owner() != this.destroyer.owner()) + .filter(u => !u.owner().isAlliedWith(this.destroyer.owner())) + if (ships.length == 0) { + return + } + // TODO: sort by distance + this.target = ships[0] + } + const next = this.pathfinder.nextTile(this.destroyer.tile(), this.target.tile()) + if (next == null) { + this.target = null + return + } + + this.destroyer.move(next) } owner(): MutablePlayer { From 4e71a64ea71927c5cf32c233444a17bf23db7998 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 11 Nov 2024 20:37:41 -0800 Subject: [PATCH 07/25] implement destroyers --- src/core/PathFinding.ts | 35 +++++++++++++++++++---- src/core/execution/DestroyerExecution.ts | 36 ++++++++++++++++++++---- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/core/PathFinding.ts b/src/core/PathFinding.ts index 7db19b4ff..03cd9f70b 100644 --- a/src/core/PathFinding.ts +++ b/src/core/PathFinding.ts @@ -1,5 +1,6 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; import { Tile } from "./game/Game"; +import { manhattanDist } from "./Util"; export class AStar { @@ -82,20 +83,44 @@ export class PathFinder { } nextTile(curr: Tile, dst: Tile): Tile { - if (curr != this.curr || dst != this.dst || this.path == null) { + if (this.shouldRecompute(curr, dst)) { this.curr = curr this.dst = dst this.path = null this.aStar = new AStar(curr, dst) - if (this.aStar.compute(1000)) { + if (this.aStar.compute(50000)) { this.path = this.aStar.reconstructPath() } else { return null } - } - if (this.path.length == 0) { - return null + if (this.path.length > 0) { + this.path.shift() + } } return this.path.shift() } + + private shouldRecompute(curr: Tile, dst: Tile) { + if (this.path == null || this.curr == null || this.dst == null) { + return true + } + const dist = manhattanDist(curr.cell(), dst.cell()) + let tolerance = 10 + if (dist > 50) { + tolerance = 10 + } else if (dist > 25) { + tolerance = 5 + } else if (dist > 10) { + tolerance = 3 + } else { + tolerance = 0 + } + if (manhattanDist(this.curr.cell(), curr.cell()) > tolerance) { + return true + } + if (manhattanDist(this.dst.cell(), dst.cell()) > tolerance) { + return true + } + return false + } } diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index 3b5233da8..d1732bd97 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -12,6 +12,8 @@ export class DestroyerExecution implements Execution { private target: MutableUnit = null private pathfinder = new PathFinder() + private patrolTile: Tile; + // TODO: put in config private searchRange = 100 @@ -24,9 +26,11 @@ export class DestroyerExecution implements Execution { init(mg: MutableGame, ticks: number): void { this._owner = mg.player(this.playerID) this.mg = mg + this.patrolTile = mg.tile(this.cell) } tick(ticks: number): void { + // TODO: remove gold from player if (this.destroyer == null) { this.destroyer = this._owner.addUnit(UnitType.Destroyer, 0, this.mg.tile(this.cell)) return @@ -35,24 +39,46 @@ export class DestroyerExecution implements Execution { this.active = false return } + if (this.target != null && !this.target.isActive()) { + this.target = null + } if (this.target == null) { const ships = this.mg.units(UnitType.TransportShip) - .filter(u => manhattanDist(u.tile().cell(), this.destroyer.tile().cell())) - .filter(u => u.owner() != this.destroyer.owner()) + .filter(u => manhattanDist(u.tile().cell(), this.destroyer.tile().cell()) < 100) + // .filter(u => u.owner() != this.destroyer.owner()) + .filter(u => u != this.destroyer) .filter(u => !u.owner().isAlliedWith(this.destroyer.owner())) if (ships.length == 0) { + if (manhattanDist(this.destroyer.tile().cell(), this.cell) > 5) { + for (let i = 0; i < 1 + this.mg.ticks() % 2; i++) { + const next = this.pathfinder.nextTile(this.destroyer.tile(), this.patrolTile) + if (next == null) { + this.target = null + return + } + this.destroyer.move(next) + } + } return } // TODO: sort by distance this.target = ships[0] } - const next = this.pathfinder.nextTile(this.destroyer.tile(), this.target.tile()) - if (next == null) { + if (manhattanDist(this.destroyer.tile().cell(), this.target.tile().cell()) < 5) { + this.target.delete() this.target = null return } + for (let i = 0; i < 1 + this.mg.ticks() % 2; i++) { + const next = this.pathfinder.nextTile(this.destroyer.tile(), this.target.tile()) + if (next == null) { + this.target = null + console.warn(`target not found`) + return + } + this.destroyer.move(next) + } - this.destroyer.move(next) } owner(): MutablePlayer { From c399cb59fb15e0e52539bc750707b03b581073ff Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 12 Nov 2024 16:32:48 -0800 Subject: [PATCH 08/25] perf improvement a star --- src/core/PathFinding.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/core/PathFinding.ts b/src/core/PathFinding.ts index 03cd9f70b..2b31c53db 100644 --- a/src/core/PathFinding.ts +++ b/src/core/PathFinding.ts @@ -41,7 +41,7 @@ export class AStar { for (const neighbor of this.current.neighborsWrapped()) { if (neighbor != this.dst && neighbor.isLand()) continue; // Skip non-water tiles - const tentativeGScore = this.gScore.get(this.current)! + 100 - neighbor.magnitude(); + const tentativeGScore = this.gScore.get(this.current)! + 9 - Math.max(1, Math.min(neighbor.magnitude(), 8)); if (!this.gScore.has(neighbor) || tentativeGScore < this.gScore.get(neighbor)!) { this.cameFrom.set(neighbor, this.current); @@ -88,7 +88,7 @@ export class PathFinder { this.dst = dst this.path = null this.aStar = new AStar(curr, dst) - if (this.aStar.compute(50000)) { + if (this.aStar.compute(5000)) { this.path = this.aStar.reconstructPath() } else { return null @@ -115,9 +115,6 @@ export class PathFinder { } else { tolerance = 0 } - if (manhattanDist(this.curr.cell(), curr.cell()) > tolerance) { - return true - } if (manhattanDist(this.dst.cell(), dst.cell()) > tolerance) { return true } From 0b3a92e498b73bcd18e99bb15f6ac16d11fa4fba Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 12 Nov 2024 16:51:10 -0800 Subject: [PATCH 09/25] transport ship use pathfinding library --- src/core/PathFinding.ts | 4 +- src/core/execution/DestroyerExecution.ts | 2 +- src/core/execution/TransportShipExecution.ts | 39 +++++++------------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/core/PathFinding.ts b/src/core/PathFinding.ts index 2b31c53db..e3a871ea0 100644 --- a/src/core/PathFinding.ts +++ b/src/core/PathFinding.ts @@ -78,7 +78,7 @@ export class PathFinder { private path: Tile[] private aStar: AStar - constructor() { + constructor(private iterations: number) { } @@ -88,7 +88,7 @@ export class PathFinder { this.dst = dst this.path = null this.aStar = new AStar(curr, dst) - if (this.aStar.compute(5000)) { + if (this.aStar.compute(this.iterations)) { this.path = this.aStar.reconstructPath() } else { return null diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index d1732bd97..2a318d61c 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -10,7 +10,7 @@ export class DestroyerExecution implements Execution { private mg: MutableGame = null private target: MutableUnit = null - private pathfinder = new PathFinder() + private pathfinder = new PathFinder(5000) private patrolTile: Tile; diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 94dc04a5b..64c3e18d0 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -2,7 +2,7 @@ import { Unit, Cell, Execution, MutableUnit, MutableGame, MutablePlayer, Player, import { and, bfs, manhattanDistWrapped, sourceDstOceanShore } from "../Util"; import { AttackExecution } from "./AttackExecution"; import { DisplayMessageEvent, MessageType } from "../../client/graphics/layers/EventsDisplay"; -import { AStar } from "../PathFinding"; +import { AStar, PathFinder } from "../PathFinding"; export class TransportShipExecution implements Execution { @@ -22,14 +22,10 @@ export class TransportShipExecution implements Execution { private src: Tile | null private dst: Tile | null - private currTileIndex: number = 0 private boat: MutableUnit - private aStarPre: AStar - private aStarComplete: AStar - - private finalPath = false + private pathFinder: PathFinder = new PathFinder(100_000) constructor( private attackerID: PlayerID, @@ -82,16 +78,8 @@ export class TransportShipExecution implements Execution { return } - this.aStarPre = new AStar(this.src, this.dst) - this.aStarPre.compute(5) - this.path = this.aStarPre.reconstructPath() - if (this.path != null) { - this.boat = this.attacker.addUnit(UnitType.TransportShip, this.troops, this.src) - } else { - console.log('got null path') - this.active = false - } - this.aStarComplete = new AStar(this.path[this.path.length - 1], this.dst) + + this.boat = this.attacker.addUnit(UnitType.TransportShip, this.troops, this.src) } tick(ticks: number) { @@ -107,15 +95,8 @@ export class TransportShipExecution implements Execution { } this.lastMove = ticks - if (!this.finalPath && this.aStarComplete.compute(30000)) { - this.path.push(...this.aStarComplete.reconstructPath()) - this.finalPath = true - } - if (this.currTileIndex >= this.path.length) { - if (!this.finalPath) { - return - } + if (this.boat.tile() == this.dst) { if (this.dst.owner() == this.attacker) { this.attacker.addTroops(this.troops) this.boat.delete() @@ -135,9 +116,15 @@ export class TransportShipExecution implements Execution { return } - const nextTile = this.path[this.currTileIndex] + const nextTile = this.pathFinder.nextTile(this.boat.tile(), this.dst) + if (nextTile == null) { + console.warn('path not found') + this.attacker.addTroops(this.boat.troops()) + this.boat.delete() + this.active = false + return + } this.boat.move(nextTile) - this.currTileIndex++ } owner(): MutablePlayer { From 36c9d67729f214059a8c61ff74dcedde5d82c1ae Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 12 Nov 2024 18:34:39 -0800 Subject: [PATCH 10/25] pathfinding perf improvement --- src/core/PathFinding.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/PathFinding.ts b/src/core/PathFinding.ts index e3a871ea0..b7bb778c9 100644 --- a/src/core/PathFinding.ts +++ b/src/core/PathFinding.ts @@ -41,7 +41,7 @@ export class AStar { for (const neighbor of this.current.neighborsWrapped()) { if (neighbor != this.dst && neighbor.isLand()) continue; // Skip non-water tiles - const tentativeGScore = this.gScore.get(this.current)! + 9 - Math.max(1, Math.min(neighbor.magnitude(), 8)); + const tentativeGScore = this.gScore.get(this.current)! + 3 - Math.max(1, Math.min(neighbor.magnitude() / 4, 2)); if (!this.gScore.has(neighbor) || tentativeGScore < this.gScore.get(neighbor)!) { this.cameFrom.set(neighbor, this.current); From 49b35b0e4519a30d351eeea204c38fd26080fa26 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Tue, 12 Nov 2024 19:26:12 -0800 Subject: [PATCH 11/25] update todo --- TODO.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/TODO.txt b/TODO.txt index 6faccdcdf..e08b86a89 100644 --- a/TODO.txt +++ b/TODO.txt @@ -176,7 +176,10 @@ * add troop/worker slider DONE 11/4/2024 * create Unit layer DONE 11/9/2024 * create Unit interface DONE 11/10/2024 -* add destroyer +* add destroyer DONE 11/12/2024 +* add ports +* destroyer spawn from port +* add trade ship * NPC has relations * use twitter emojis * private game shows how many players joined From 4236a580e3e8da22c4c646361ce7ce02d6435a93 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 12 Nov 2024 20:49:24 -0800 Subject: [PATCH 12/25] working on port --- resources/images/AnchorIcon.png | Bin 0 -> 143 bytes resources/images/PortIcon.svg | 40 ++++++++++++++++++ src/client/Transport.ts | 14 +++--- src/client/graphics/layers/UnitLayer.ts | 2 + .../graphics/layers/radial/BuildMenu.ts | 12 +++--- src/core/Schemas.ts | 15 ++++--- src/core/execution/ExecutionManager.ts | 14 ++++-- src/core/execution/PortExecution.ts | 31 ++++++++++++++ src/core/game/Game.ts | 6 ++- 9 files changed, 111 insertions(+), 23 deletions(-) create mode 100644 resources/images/AnchorIcon.png create mode 100644 resources/images/PortIcon.svg create mode 100644 src/core/execution/PortExecution.ts diff --git a/resources/images/AnchorIcon.png b/resources/images/AnchorIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..75a031b755f362df6d790a2b95f57245f1600b5f GIT binary patch literal 143 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>@$__Y4ABTq zPC3Bxf1y*)i69LbHa4~iN;4#kr1dy1`)qNVz_w`3vDE@cbuE1U@E2Klayf=6{T6HP p_Z7NnTwqpYafs#OVum$r45iK@90rFczX2M`;OXk;vd$@?2>?sCDgpoi literal 0 HcmV?d00001 diff --git a/resources/images/PortIcon.svg b/resources/images/PortIcon.svg new file mode 100644 index 000000000..b3e417cd6 --- /dev/null +++ b/resources/images/PortIcon.svg @@ -0,0 +1,40 @@ + + diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 0098b54d6..208a7f098 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -1,7 +1,7 @@ import { Config } from "../core/configuration/Config" import { EventBus, GameEvent } from "../core/EventBus" -import { AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType, Tile } from "../core/game/Game" -import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, CreateDestroyerIntent, GameID, Intent, ServerMessage, ServerMessageSchema } from "../core/Schemas" +import { AllianceRequest, AllPlayers, Cell, Item, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game" +import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, BuildUnitIntentSchema, GameID, Intent, ServerMessage, ServerMessageSchema } from "../core/Schemas" import { LocalServer } from "./LocalServer" @@ -47,8 +47,9 @@ export class SendBoatAttackIntentEvent implements GameEvent { ) { } } -export class SendCreateDestroyerIntentEvent implements GameEvent { +export class BuildUnitIntentEvent implements GameEvent { constructor( + public readonly unit: UnitType, public readonly cell: Cell, ) { } } @@ -121,7 +122,7 @@ export class Transport { this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e)) this.eventBus.on(SendNukeIntentEvent, (e) => this.onSendNukeIntent(e)) this.eventBus.on(SendSetTargetTroopRatioEvent, (e) => this.onSendSetTargetTroopRatioEvent(e)) - this.eventBus.on(SendCreateDestroyerIntentEvent, (e) => this.onCreateDestroyerIntent(e)) + this.eventBus.on(BuildUnitIntentEvent, (e) => this.onCreateDestroyerIntent(e)) } connect(onconnect: () => void, onmessage: (message: ServerMessage) => void) { @@ -321,11 +322,12 @@ export class Transport { }) } - private onCreateDestroyerIntent(event: SendCreateDestroyerIntentEvent) { + private onCreateDestroyerIntent(event: BuildUnitIntentEvent) { this.sendIntent({ - type: "create_destroyer", + type: "build_unit", clientID: this.clientID, player: this.playerID, + unit: event.unit, x: event.cell.x, y: event.cell.y, }) diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index c2faf248e..f0ebe0301 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -5,6 +5,8 @@ import { bfs, dist, euclDist } from "../../../core/Util"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; +import anchorIncon from '../../../../../resources/images/AnchorIcon.png'; + export class UnitLayer implements Layer { private canvas: HTMLCanvasElement private context: CanvasRenderingContext2D diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 21fb6e44f..43ac74632 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -1,11 +1,12 @@ import { LitElement, html, css } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { EventBus } from '../../../../core/EventBus'; -import { Cell, Game, Item, Items, Player } from '../../../../core/game/Game'; -import { SendCreateDestroyerIntentEvent, SendNukeIntentEvent } from '../../../Transport'; +import { Cell, Game, Item, Items, Player, UnitType } from '../../../../core/game/Game'; +import { BuildUnitIntentEvent as BuildItemIntentEvent, BuildUnitIntentEvent, SendNukeIntentEvent } from '../../../Transport'; import nukeIcon from '../../../../../resources/images/NukeIconWhite.svg'; import destroyerIcon from '../../../../../resources/images/DestroyerIconWhite.svg'; import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg'; +import portIcon from '../../../../../resources/images/PortIcon.svg'; import { renderNumber } from '../../Utils'; import { ContextMenuEvent } from '../../../InputHandler'; @@ -18,7 +19,7 @@ const buildTable: BuildItem[][] = [ [ { item: Items.Nuke, icon: nukeIcon }, { item: Items.Destroyer, icon: destroyerIcon }, - // { id: 'battleship', name: 'Battleship', icon: '🚢', cost: 500, buildTime: 20 } + { item: Items.Port, icon: portIcon } ] ]; @@ -154,8 +155,9 @@ export class BuildMenu extends LitElement { this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null)) break case "Destroyer": - this.eventBus.emit(new SendCreateDestroyerIntentEvent(this.clickedCell)) - + this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Destroyer, this.clickedCell)) + case "Port": + this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Port, this.clickedCell)) } this.hideMenu() }; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 8d4b1be04..11133fbd6 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { Difficulty, GameMap, PlayerType } from './game/Game'; +import { Difficulty, GameMap, PlayerType, UnitType } from './game/Game'; export type GameID = string export type ClientID = string @@ -15,7 +15,7 @@ export type Intent = SpawnIntent | DonateIntent | NukeIntent | TargetTroopRatioIntent - | CreateDestroyerIntent + | BuildUnitIntent export type AttackIntent = z.infer export type SpawnIntent = z.infer @@ -28,7 +28,7 @@ export type EmojiIntent = z.infer export type DonateIntent = z.infer export type NukeIntent = z.infer export type TargetTroopRatioIntent = z.infer -export type CreateDestroyerIntent = z.infer +export type BuildUnitIntent = z.infer export type Turn = z.infer export type GameConfig = z.infer @@ -69,7 +69,7 @@ const EmojiSchema = z.string().refine( ); // Zod schemas const BaseIntentSchema = z.object({ - type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'nuke', 'troop_ratio', 'create_destroyer']), + type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji', 'nuke', 'troop_ratio', 'build_unit']), clientID: z.string(), }); @@ -161,9 +161,10 @@ export const TargetTroopRatioIntentSchema = BaseIntentSchema.extend({ ratio: z.number().min(0).max(1), }) -export const CreateDestroyerIntentSchema = BaseIntentSchema.extend({ - type: z.literal('create_destroyer'), +export const BuildUnitIntentSchema = BaseIntentSchema.extend({ + type: z.literal('build_unit'), player: z.string(), + unit: z.nativeEnum(UnitType), x: z.number(), y: z.number(), }) @@ -180,7 +181,7 @@ const IntentSchema = z.union([ DonateIntentSchema, NukeIntentSchema, TargetTroopRatioIntentSchema, - CreateDestroyerIntentSchema, + BuildUnitIntentSchema, ]); const TurnSchema = z.object({ diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 49575021e..1f860b57a 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -1,4 +1,4 @@ -import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile, PlayerType, Alliance, AllianceRequestReplyEvent, Difficulty } from "../game/Game"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerInfo, TerraNullius, Tile, PlayerType, Alliance, AllianceRequestReplyEvent, Difficulty, UnitType } from "../game/Game"; import { AttackIntent, BoatAttackIntentSchema, GameID, Intent, Turn } from "../Schemas"; import { AttackExecution } from "./AttackExecution"; import { SpawnExecution } from "./SpawnExecution"; @@ -17,6 +17,7 @@ import { DonateExecution } from "./DonateExecution"; import { NukeExecution } from "./NukeExecution"; import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution"; import { DestroyerExecution } from "./DestroyerExecution"; +import { PortExecution } from "./PortExecution"; @@ -81,8 +82,15 @@ export class Executor { return new NukeExecution(intent.sender, new Cell(intent.x, intent.y), intent.magnitude); case "troop_ratio": return new SetTargetTroopRatioExecution(intent.player, intent.ratio); - case "create_destroyer": - return new DestroyerExecution(intent.player, new Cell(intent.x, intent.y)) + case "build_unit": + switch (intent.unit) { + case UnitType.Destroyer: + return new DestroyerExecution(intent.player, new Cell(intent.x, intent.y)) + case UnitType.Port: + return new PortExecution(intent.player, new Cell(intent.x, intent.y)) + default: + throw Error(`unit type ${intent.unit} not supported`) + } default: throw new Error(`intent type ${intent} not found`); } diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts new file mode 100644 index 000000000..73504f4e0 --- /dev/null +++ b/src/core/execution/PortExecution.ts @@ -0,0 +1,31 @@ +import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, PlayerID } from "../game/Game"; + +export class PortExecution implements Execution { + + private active = true + + constructor( + private _owner: PlayerID, + private cell: Cell + ) { } + + + init(mg: MutableGame, ticks: number): void { + } + + tick(ticks: number): void { + } + + owner(): MutablePlayer { + return null + } + + isActive(): boolean { + return this.active + } + + activeDuringSpawnPhase(): boolean { + return false + } + +} \ No newline at end of file diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 7a20e73cd..0976b5744 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -27,7 +27,8 @@ export enum GameMap { export enum UnitType { TransportShip, - Destroyer + Destroyer, + Port } export class Item { @@ -36,7 +37,8 @@ export class Item { export const Items = { Nuke: new Item("Nuke", 1_000_000), - Destroyer: new Item("Destroyer", 10) + Destroyer: new Item("Destroyer", 10), + Port: new Item("Port", 10) } as const; export class Nation { From 09ead3791ddd64714849071c4dfe35080f869788 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 13 Nov 2024 15:08:51 -0800 Subject: [PATCH 13/25] add port --- src/client/graphics/layers/UnitLayer.ts | 142 +++++++++++++++++------- src/core/execution/PortExecution.ts | 8 +- src/core/game/Game.ts | 2 +- 3 files changed, 107 insertions(+), 45 deletions(-) diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index f0ebe0301..5520f4085 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -5,23 +5,34 @@ import { bfs, dist, euclDist } from "../../../core/Util"; import { Layer } from "./Layer"; import { EventBus } from "../../../core/EventBus"; -import anchorIncon from '../../../../../resources/images/AnchorIcon.png'; +import anchorIcon from '../../../../resources/images/AnchorIcon.png'; export class UnitLayer implements Layer { - private canvas: HTMLCanvasElement - private context: CanvasRenderingContext2D - private imageData: ImageData + private canvas: HTMLCanvasElement; + private context: CanvasRenderingContext2D; + private imageData: ImageData; + private anchorImage: HTMLImageElement; + private anchorImageLoaded: boolean = false; - private boatToTrail = new Map>() + private boatToTrail = new Map>(); - private theme: Theme = null + private theme: Theme = null; constructor(private game: Game, private eventBus: EventBus) { - this.theme = game.config().theme() + this.theme = game.config().theme(); + this.loadAnchorImage(); + } + + private loadAnchorImage() { + this.anchorImage = new Image(); + this.anchorImage.onload = () => { + this.anchorImageLoaded = true; + }; + this.anchorImage.src = anchorIcon; } shouldTransform(): boolean { - return true + return true; } tick() { @@ -29,23 +40,23 @@ export class UnitLayer implements Layer { init(game: Game) { this.canvas = document.createElement('canvas'); - this.context = this.canvas.getContext("2d") + this.context = this.canvas.getContext("2d"); - this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height()) + this.imageData = this.context.getImageData(0, 0, this.game.width(), this.game.height()); this.canvas.width = this.game.width(); this.canvas.height = this.game.height(); this.context.putImageData(this.imageData, 0, 0); - this.initImageData() + this.initImageData(); - this.eventBus.on(UnitEvent, e => this.onUnitEvent(e)) + this.eventBus.on(UnitEvent, e => this.onUnitEvent(e)); } initImageData() { this.game.forEachTile((tile) => { - const index = (tile.cell().y * this.game.width()) + tile.cell().x - const offset = index * 4 - this.imageData.data[offset + 3] = 0 - }) + const index = (tile.cell().y * this.game.width()) + tile.cell().x; + const offset = index * 4; + this.imageData.data[offset + 3] = 0; + }); } renderLayer(context: CanvasRenderingContext2D) { @@ -56,68 +67,115 @@ export class UnitLayer implements Layer { -this.game.height() / 2, this.game.width(), this.game.height() - ) + ); } + private handlePortEvent(event: UnitEvent) { + if (!this.anchorImageLoaded) return; + + // Create a temporary canvas to process the anchor icon + const tempCanvas = document.createElement('canvas'); + const tempContext = tempCanvas.getContext('2d'); + tempCanvas.width = this.anchorImage.width; + tempCanvas.height = this.anchorImage.height; + + // Draw the anchor icon to the temporary canvas + tempContext.drawImage(this.anchorImage, 0, 0); + const iconData = tempContext.getImageData(0, 0, tempCanvas.width, tempCanvas.height); + + // Calculate position to center the icon on the port + const cell = event.unit.tile().cell(); + const startX = cell.x - Math.floor(tempCanvas.width / 2); + const startY = cell.y - Math.floor(tempCanvas.height / 2); + + bfs(event.unit.tile(), euclDist(event.unit.tile(), 8)) + .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 255)); + // Process each pixel of the icon + for (let y = 0; y < tempCanvas.height; y++) { + for (let x = 0; x < tempCanvas.width; x++) { + const iconIndex = (y * tempCanvas.width + x) * 4; + const alpha = iconData.data[iconIndex + 3]; + + if (alpha > 0) { // Only process non-transparent pixels + const targetX = startX + x; + const targetY = startY + y; + + // Check if the target pixel is within the game bounds + if (targetX >= 0 && targetX < this.game.width() && + targetY >= 0 && targetY < this.game.height()) { + + // Color the pixel using the unit owner's colors + this.paintCell( + new Cell(targetX, targetY), + this.theme.borderColor(event.unit.owner().info()), + alpha + ); + } + } + } + } + } onUnitEvent(event: UnitEvent) { switch (event.unit.type()) { case UnitType.TransportShip: - this.handleBoatEvent(event) - break + this.handleBoatEvent(event); + break; case UnitType.Destroyer: - this.handleDestroyerEvent(event) - break + this.handleDestroyerEvent(event); + break; + case UnitType.Port: + this.handlePortEvent(event); + break; default: - throw Error(`event for unit ${event.unit.type()} not supported`) + throw Error(`event for unit ${event.unit.type()} not supported`); } } private handleDestroyerEvent(event: UnitEvent) { bfs(event.oldTile, euclDist(event.oldTile, 3)).forEach(t => { - this.clearCell(t.cell()) - }) + this.clearCell(t.cell()); + }); bfs(event.unit.tile(), euclDist(event.unit.tile(), 3)) - .forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255)) + .forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255)); bfs(event.unit.tile(), euclDist(event.unit.tile(), 2)) - .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 180)) + .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 180)); } private handleBoatEvent(event: UnitEvent) { if (!this.boatToTrail.has(event.unit)) { - this.boatToTrail.set(event.unit, new Set()) + this.boatToTrail.set(event.unit, new Set()); } - const trail = this.boatToTrail.get(event.unit) - trail.add(event.oldTile) + const trail = this.boatToTrail.get(event.unit); + trail.add(event.oldTile); bfs(event.oldTile, dist(event.oldTile, 3)).forEach(t => { - this.clearCell(t.cell()) - }) + 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) + this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 150); } } - ) + ); bfs(event.unit.tile(), dist(event.unit.tile(), 2)) - .forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255)) + .forEach(t => this.paintCell(t.cell(), this.theme.borderColor(event.unit.owner().info()), 255)); bfs(event.unit.tile(), dist(event.unit.tile(), 1)) - .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 180)) + .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 180)); } else { - trail.forEach(t => this.clearCell(t.cell())) - this.boatToTrail.delete(event.unit) + trail.forEach(t => this.clearCell(t.cell())); + this.boatToTrail.delete(event.unit); } } - paintCell(cell: Cell, color: Colord, alpha: number) { - const index = (cell.y * this.game.width()) + cell.x - const offset = index * 4 + const index = (cell.y * this.game.width()) + cell.x; + const offset = index * 4; this.imageData.data[offset] = color.rgba.r; this.imageData.data[offset + 1] = color.rgba.g; this.imageData.data[offset + 2] = color.rgba.b; - this.imageData.data[offset + 3] = alpha + this.imageData.data[offset + 3] = alpha; } clearCell(cell: Cell) { @@ -125,6 +183,4 @@ export class UnitLayer implements Layer { const offset = index * 4; this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent) } - - } \ No newline at end of file diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 73504f4e0..24ad7785a 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -1,8 +1,10 @@ -import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, PlayerID } from "../game/Game"; +import { AllPlayers, Cell, Execution, MutableGame, MutablePlayer, PlayerID, UnitType } from "../game/Game"; export class PortExecution implements Execution { private active = true + private mg: MutableGame + private player: MutablePlayer constructor( private _owner: PlayerID, @@ -11,9 +13,13 @@ export class PortExecution implements Execution { init(mg: MutableGame, ticks: number): void { + this.mg = mg + this.player = mg.player(this._owner) } tick(ticks: number): void { + this.player.addUnit(UnitType.Port, 0, this.mg.tile(this.cell)) + this.active = false } owner(): MutablePlayer { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 0976b5744..d002c308e 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -38,7 +38,7 @@ export class Item { export const Items = { Nuke: new Item("Nuke", 1_000_000), Destroyer: new Item("Destroyer", 10), - Port: new Item("Port", 10) + Port: new Item("Port", 0) } as const; export class Nation { From febabf00db646496277b3c718b76fa60ab9c06d1 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 13 Nov 2024 15:31:31 -0800 Subject: [PATCH 14/25] must build port before destroyer --- .../graphics/layers/radial/BuildMenu.ts | 38 ++++++++++++------- src/core/execution/NukeExecution.ts | 6 +-- src/core/game/Game.ts | 19 ++++++---- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 43ac74632..0f174e941 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -1,7 +1,7 @@ import { LitElement, html, css } from 'lit'; import { customElement, state } from 'lit/decorators.js'; import { EventBus } from '../../../../core/EventBus'; -import { Cell, Game, Item, Items, Player, UnitType } from '../../../../core/game/Game'; +import { Cell, Game, Item, BuildItems, Player, UnitType } from '../../../../core/game/Game'; import { BuildUnitIntentEvent as BuildItemIntentEvent, BuildUnitIntentEvent, SendNukeIntentEvent } from '../../../Transport'; import nukeIcon from '../../../../../resources/images/NukeIconWhite.svg'; import destroyerIcon from '../../../../../resources/images/DestroyerIconWhite.svg'; @@ -17,9 +17,9 @@ interface BuildItem { const buildTable: BuildItem[][] = [ [ - { item: Items.Nuke, icon: nukeIcon }, - { item: Items.Destroyer, icon: destroyerIcon }, - { item: Items.Port, icon: portIcon } + { item: BuildItems.Nuke, icon: nukeIcon }, + { item: BuildItems.Destroyer, icon: destroyerIcon }, + { item: BuildItems.Port, icon: portIcon } ] ]; @@ -145,19 +145,29 @@ export class BuildMenu extends LitElement { @state() private _hidden = true; - private canAfford(item: BuildItem): boolean { - return this.myPlayer && this.myPlayer.gold() >= item.item.cost; + private canBuild(item: BuildItem): boolean { + if (!this.myPlayer || this.myPlayer.gold() < item.item.cost) { + return false + } + switch (item.item) { + case BuildItems.Destroyer: + return this.myPlayer.units(UnitType.Port).length > 0 + default: + return true + } } public onBuildSelected = (item: BuildItem) => { - switch (item.item.name) { - case "Nuke": + switch (item.item) { + case BuildItems.Nuke: this.eventBus.emit(new SendNukeIntentEvent(this.myPlayer, this.clickedCell, null)) break - case "Destroyer": + case BuildItems.Destroyer: this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Destroyer, this.clickedCell)) - case "Port": + break + case BuildItems.Port: this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Port, this.clickedCell)) + break } this.hideMenu() }; @@ -171,11 +181,11 @@ export class BuildMenu extends LitElement {