From 8abc5e4aed8a04c5f8d62759ba8d951db9cbf444 Mon Sep 17 00:00:00 2001 From: Evan Date: Tue, 26 Nov 2024 10:15:54 -0800 Subject: [PATCH] implement battleship --- TODO.txt | 6 +- resources/images/BattleshipIconWhite.svg | 86 +++++++++++++ src/client/graphics/layers/UnitLayer.ts | 14 +++ .../graphics/layers/radial/BuildMenu.ts | 20 +-- src/core/configuration/DefaultConfig.ts | 12 +- src/core/configuration/DevConfig.ts | 2 +- src/core/execution/BattleshipExecution.ts | 117 ++++++++++++++++++ src/core/execution/DestroyerExecution.ts | 2 +- src/core/execution/ExecutionManager.ts | 3 + src/core/execution/ShellExecution.ts | 58 +++++++++ src/core/game/Game.ts | 2 + src/core/game/PlayerImpl.ts | 7 +- 12 files changed, 304 insertions(+), 25 deletions(-) create mode 100644 resources/images/BattleshipIconWhite.svg create mode 100644 src/core/execution/BattleshipExecution.ts create mode 100644 src/core/execution/ShellExecution.ts diff --git a/TODO.txt b/TODO.txt index 768679956..70e94a0e3 100644 --- a/TODO.txt +++ b/TODO.txt @@ -195,10 +195,10 @@ * BUG: destroys destroy trade ships instead of capturing them DONE 11/25/2024 * nukes break alliance DONE 11/25/2024 * BUG: nuke yourself makes you traitor DONE 11/25/2024 -* don't capture trade ships if allied with either port -* make ports cost more for more ports -* NPC has relations +* make ports cost more for more ports DONE 11/25/2024 * add battleship +* cache images in UnitLayer +* NPC has relations * add defense post * add radiation from nuke * only show units you can build in the build menu diff --git a/resources/images/BattleshipIconWhite.svg b/resources/images/BattleshipIconWhite.svg new file mode 100644 index 000000000..45532ed63 --- /dev/null +++ b/resources/images/BattleshipIconWhite.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + diff --git a/src/client/graphics/layers/UnitLayer.ts b/src/client/graphics/layers/UnitLayer.ts index dcdaa5240..6ea66048f 100644 --- a/src/client/graphics/layers/UnitLayer.ts +++ b/src/client/graphics/layers/UnitLayer.ts @@ -78,6 +78,12 @@ export class UnitLayer implements Layer { case UnitType.Destroyer: this.handleDestroyerEvent(event); break; + case UnitType.Battleship: + this.handleBattleshipEvent(event); + break; + case UnitType.Shell: + this.handleShellEvent(event) + break; case UnitType.TradeShip: this.handleTradeShipEvent(event) break; @@ -116,6 +122,14 @@ export class UnitLayer implements Layer { .forEach(t => this.paintCell(t.cell(), this.theme.territoryColor(event.unit.owner().info()), 255)); } + private handleShellEvent(event: UnitEvent) { + this.clearCell(event.oldTile.cell()) + if (!event.unit.isActive()) { + return + } + this.paintCell(event.unit.tile().cell(), this.theme.borderColor(event.unit.owner().info()), 255) + } + private handleNuke(event: UnitEvent) { bfs(event.oldTile, euclDist(event.oldTile, 2)).forEach(t => { diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index 89314af73..29d7ebc5b 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -6,6 +6,7 @@ import { BuildUnitIntentEvent } from '../../../Transport'; import atomBombIcon from '../../../../../resources/images/NukeIconWhite.svg'; import hydrogenBombIcon from '../../../../../resources/images/MushroomCloudIconWhite.svg'; import destroyerIcon from '../../../../../resources/images/DestroyerIconWhite.svg'; +import battleshipIcon from '../../../../../resources/images/BattleshipIconWhite.svg'; import missileSiloIcon from '../../../../../resources/images/MissileSiloIconWhite.svg'; import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg'; import portIcon from '../../../../../resources/images/PortIcon.svg'; @@ -22,6 +23,7 @@ const buildTable: BuildItemDisplay[][] = [ { unitType: UnitType.AtomBomb, icon: atomBombIcon }, { unitType: UnitType.HydrogenBomb, icon: hydrogenBombIcon }, { unitType: UnitType.Destroyer, icon: destroyerIcon }, + { unitType: UnitType.Battleship, icon: battleshipIcon }, { unitType: UnitType.Port, icon: portIcon }, { unitType: UnitType.MissileSilo, icon: missileSiloIcon } ] @@ -156,23 +158,7 @@ export class BuildMenu extends LitElement { } public onBuildSelected = (item: BuildItemDisplay) => { - switch (item.unitType) { - case UnitType.AtomBomb: - this.eventBus.emit(new BuildUnitIntentEvent(UnitType.AtomBomb, this.clickedCell)) - break - case UnitType.HydrogenBomb: - this.eventBus.emit(new BuildUnitIntentEvent(UnitType.HydrogenBomb, this.clickedCell)) - break - case UnitType.Destroyer: - this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Destroyer, this.clickedCell)) - break - case UnitType.Port: - this.eventBus.emit(new BuildUnitIntentEvent(UnitType.Port, this.clickedCell)) - break - case UnitType.MissileSilo: - this.eventBus.emit(new BuildUnitIntentEvent(UnitType.MissileSilo, this.clickedCell)) - break - } + this.eventBus.emit(new BuildUnitIntentEvent(item.unitType, this.clickedCell)) this.hideMenu() }; diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 117e1926b..44875c2c8 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -29,9 +29,19 @@ export class DefaultConfig implements Config { cost: (p: Player) => (p.units(UnitType.Destroyer).length + 1) * 250_000, territoryBound: false } + case UnitType.Battleship: + return { + cost: (p: Player) => (p.units(UnitType.Battleship).length + 1) * 500_000, + territoryBound: false + } + case UnitType.Shell: + return { + cost: (p: Player) => (p.units(UnitType.Destroyer).length + 1) * 500_000, + territoryBound: false + } case UnitType.Port: return { - cost: (p: Player) => (p.units(UnitType.Port).length + 1) * 250_000, + cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 250_000, territoryBound: true } case UnitType.AtomBomb: diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index f77599259..72598cbf0 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -13,7 +13,7 @@ export const devConfig = new class extends DefaultConfig { return 95 } numSpawnPhaseTurns(): number { - return 40 + return 80 } gameCreationRate(): number { return 20 * 1000 diff --git a/src/core/execution/BattleshipExecution.ts b/src/core/execution/BattleshipExecution.ts new file mode 100644 index 000000000..aba4f021a --- /dev/null +++ b/src/core/execution/BattleshipExecution.ts @@ -0,0 +1,117 @@ +import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; +import { AStar, PathFinder, PathFindResultType } from "../PathFinding"; +import { PseudoRandom } from "../PseudoRandom"; +import { distSort, distSortUnit, manhattanDist } from "../Util"; +import { ShellExecution } from "./ShellExecution"; + +export class BattleshipExecution implements Execution { + private random: PseudoRandom + + private _owner: MutablePlayer + private active = true + private battleship: MutableUnit = null + private mg: MutableGame = null + + private pathfinder = new PathFinder(5000, t => t.isWater()) + + private patrolTile: Tile; + private patrolCenterTile: Tile + + // TODO: put in config + private searchRange = 100 + private attackRate = 20 + private lastAttack = 0 + + constructor( + private playerID: PlayerID, + private cell: Cell, + ) { } + + + init(mg: MutableGame, ticks: number): void { + this._owner = mg.player(this.playerID) + this.mg = mg + this.patrolCenterTile = mg.tile(this.cell) + this.patrolTile = this.patrolCenterTile + this.random = new PseudoRandom(mg.ticks()) + } + + tick(ticks: number): void { + if (this.battleship == null) { + const spawn = this._owner.canBuild(UnitType.Battleship, this.patrolTile) + if (spawn == false) { + this.active = false + return + } + this.battleship = this._owner.buildUnit(UnitType.Battleship, 0, spawn) + return + } + if (!this.battleship.isActive()) { + this.active = false + return + } + + if (this.mg.ticks() % 2 == 0) { + const result = this.pathfinder.nextTile(this.battleship.tile(), this.patrolTile) + switch (result.type) { + case PathFindResultType.Completed: + this.patrolTile = this.randomTile() + break + case PathFindResultType.NextTile: + this.battleship.move(result.tile) + break + case PathFindResultType.Pending: + return + case PathFindResultType.PathNotFound: + console.log(`path not found to patrol tile`) + this.patrolTile = this.randomTile() + break + } + } + + if (this.mg.ticks() - this.lastAttack < this.attackRate) { + return + } + + const ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship) + .filter(u => manhattanDist(u.tile().cell(), this.battleship.tile().cell()) < 100) + .filter(u => u.owner() != this.battleship.owner()) + .filter(u => u != this.battleship) + .filter(u => !u.owner().isAlliedWith(this.battleship.owner())) + .sort(distSortUnit(this.battleship)); + + if (ships.length > 0) { + this.lastAttack = this.mg.ticks() + this.mg.addExecution(new ShellExecution(this.battleship.tile(), this.battleship.owner(), ships[0])) + } + } + + owner(): MutablePlayer { + return this._owner + } + + isActive(): boolean { + return this.active + } + + activeDuringSpawnPhase(): boolean { + return false + } + + randomTile(): Tile { + while (true) { + const x = this.patrolCenterTile.cell().x + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) + const y = this.patrolCenterTile.cell().y + this.random.nextInt(-this.searchRange / 2, this.searchRange / 2) + const cell = new Cell(x, y) + if (!this.mg.isOnMap(cell)) { + continue + } + const tile = this.mg.tile(cell) + if (!tile.isOcean()) { + continue + } + return tile + } + } + +} \ No newline at end of file diff --git a/src/core/execution/DestroyerExecution.ts b/src/core/execution/DestroyerExecution.ts index 6b3e12556..c51c062e4 100644 --- a/src/core/execution/DestroyerExecution.ts +++ b/src/core/execution/DestroyerExecution.ts @@ -52,7 +52,7 @@ export class DestroyerExecution implements Execution { this.target = null } if (this.target == null) { - const ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip) + const ships = this.mg.units(UnitType.TransportShip, UnitType.Destroyer, UnitType.TradeShip, UnitType.Battleship) .filter(u => manhattanDist(u.tile().cell(), this.destroyer.tile().cell()) < 100) .filter(u => u.owner() != this.destroyer.owner()) .filter(u => u != this.destroyer) diff --git a/src/core/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 6c1e3a301..2eecae9c7 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -19,6 +19,7 @@ import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution"; import { DestroyerExecution } from "./DestroyerExecution"; import { PortExecution } from "./PortExecution"; import { MissileSiloExecution } from "./MissileSiloExecution"; +import { BattleshipExecution } from "./BattleshipExecution"; @@ -88,6 +89,8 @@ export class Executor { return new NukeExecution(intent.unit, intent.player, new Cell(intent.x, intent.y)) case UnitType.Destroyer: return new DestroyerExecution(intent.player, new Cell(intent.x, intent.y)) + case UnitType.Battleship: + return new BattleshipExecution(intent.player, new Cell(intent.x, intent.y)) case UnitType.Port: return new PortExecution(intent.player, new Cell(intent.x, intent.y)) case UnitType.MissileSilo: diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts new file mode 100644 index 000000000..65b7ec35c --- /dev/null +++ b/src/core/execution/ShellExecution.ts @@ -0,0 +1,58 @@ +import { Execution, MutableGame, MutablePlayer, MutableUnit, Tile, Unit, UnitType } from "../game/Game"; +import { PathFinder, PathFindResultType } from "../PathFinding"; + +export class ShellExecution implements Execution { + + private active = true + private pathFinder = new PathFinder(2000, () => true, 10) + private shell: MutableUnit + + constructor(private spawn: Tile, private _owner: MutablePlayer, private target: MutableUnit) { + + } + + init(mg: MutableGame, ticks: number): void { + } + + tick(ticks: number): void { + if (this.shell == null) { + this.shell = this._owner.buildUnit(UnitType.Shell, 0, this.spawn) + } + if (!this.target.isActive()) { + this.shell.delete() + this.active = false + return + } + for (let i = 0; i < 3; i++) { + const result = this.pathFinder.nextTile(this.shell.tile(), this.target.tile()) + switch (result.type) { + case PathFindResultType.Completed: + this.active = false + this.target.delete() + this.shell.delete() + return + case PathFindResultType.NextTile: + this.shell.move(result.tile) + break + case PathFindResultType.Pending: + return + case PathFindResultType.PathNotFound: + console.log(`Shell ${this.shell} could not find target`) + this.active = false + this.shell.delete() + return + } + } + } + + 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 ce0a62a2b..c8105a327 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -31,6 +31,8 @@ export interface UnitInfo { export enum UnitType { TransportShip = "Transport", Destroyer = "Destroyer", + Battleship = "Battleship", + Shell = "Shell", Port = "Port", AtomBomb = "Atom Bomb", HydrogenBomb = "Hydrogen Bomb", diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 9335be702..05c98b08c 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -357,7 +357,10 @@ export class PlayerImpl implements MutablePlayer { case UnitType.Port: return this.portSpawn(targetTile) case UnitType.Destroyer: - return this.destroyerSpawn(targetTile) + case UnitType.Battleship: + return this.warshipSpawn(targetTile) + case UnitType.Shell: + return targetTile case UnitType.MissileSilo: return this.missileSiloSpawn(targetTile) case UnitType.TransportShip: @@ -387,7 +390,7 @@ export class PlayerImpl implements MutablePlayer { return spawns[0] } - destroyerSpawn(tile: Tile): Tile | false { + warshipSpawn(tile: Tile): Tile | false { if (!tile.isOcean()) { return false }