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;