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;