From 5c51655eca0f57a85053a7e0e5e397b69947eab5 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 4 Dec 2024 13:21:35 -0800 Subject: [PATCH 1/6] add radiation --- TODO.txt | 2 +- src/client/graphics/layers/TerritoryLayer.ts | 4 + src/core/configuration/Config.ts | 2 + src/core/configuration/DefaultConfig.ts | 9 + src/core/configuration/PastelTheme.ts | 232 ++++++++++--------- src/core/execution/NukeExecution.ts | 3 + src/core/game/Game.ts | 2 + src/core/game/GameImpl.ts | 10 + src/core/game/TileImpl.ts | 6 + 9 files changed, 158 insertions(+), 112 deletions(-) diff --git a/TODO.txt b/TODO.txt index b4467f2d8..547d97b27 100644 --- a/TODO.txt +++ b/TODO.txt @@ -207,10 +207,10 @@ * use mini A* for all pathfinding DONE 12/3/2024 * bugfix: gameStop not found error DONE 12/3/2024 * log stack traces & display them on screen DONE 12/3/2024 +* add radiation from nuke * record and replay games for debugging purposes * bugfix: destroyers can't find path to dst and freeze * record single player game stats -* add radiation from nuke * add cities * create behavior tests * create perf test diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index 7a2edac37..dd8613c29 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -75,6 +75,10 @@ export class TerritoryLayer implements Layer { } paintTerritory(tile: Tile) { + if (tile.hasFallout()) { + this.paintCell(tile.cell(), this.theme.falloutColor(), 150) + return + } if (!tile.hasOwner()) { this.clearCell(tile.cell()) return diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 88cabcffd..277860ce5 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -62,6 +62,7 @@ export interface Config { tradeShipSpawnRate(): number defensePostRange(): number defensePostDefenseBonus(): number + falloutDefenseModifier(): number } export interface Theme { @@ -71,6 +72,7 @@ export interface Theme { defendedBorderColor(playerInfo: PlayerInfo): Colord; terrainColor(tile: Tile): Colord; backgroundColor(): Colord; + falloutColor(): Colord font(): string; } diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 24d81ac72..45fb05c14 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -7,6 +7,11 @@ import { pastelTheme } from "./PastelTheme"; export class DefaultConfig implements Config { + + falloutDefenseModifier(): number { + return 2 + } + defensePostRange(): number { return 30 } @@ -148,6 +153,10 @@ export class DefaultConfig implements Config { } mag *= tileToConquer.defenseBonus(attacker) speed *= tileToConquer.defenseBonus(attacker) + if (tileToConquer.hasFallout()) { + mag *= this.falloutDefenseModifier() + speed *= this.falloutDefenseModifier() + } if (attacker.isPlayer() && defender.isPlayer()) { if (attacker.type() == PlayerType.Human && defender.type() == PlayerType.Bot) { diff --git a/src/core/configuration/PastelTheme.ts b/src/core/configuration/PastelTheme.ts index 37420f38e..40cd994d8 100644 --- a/src/core/configuration/PastelTheme.ts +++ b/src/core/configuration/PastelTheme.ts @@ -1,123 +1,129 @@ -import {Colord, colord, random} from "colord"; -import {PlayerID, PlayerInfo, TerrainType, Tile} from "../game/Game"; -import {Theme} from "./Config"; -import {time} from "console"; -import {PseudoRandom} from "../PseudoRandom"; -import {simpleHash} from "../Util"; +import { Colord, colord, random } from "colord"; +import { PlayerID, PlayerInfo, TerrainType, Tile } from "../game/Game"; +import { Theme } from "./Config"; +import { time } from "console"; +import { PseudoRandom } from "../PseudoRandom"; +import { simpleHash } from "../Util"; export const pastelTheme = new class implements Theme { - + private rand = new PseudoRandom(123) - private background = colord({r: 60, g: 60, b: 60}); - private land = colord({r: 194, g: 193, b: 148}); - private shore = colord({r: 204, g: 203, b: 158}); - - private water = colord({r: 75, g: 142, b: 190}); - private shorelineWater = colord({r: 100, g: 143, b: 255}); + private background = colord({ r: 60, g: 60, b: 60 }); + private land = colord({ r: 194, g: 193, b: 148 }); + private shore = colord({ r: 204, g: 203, b: 158 }); + private falloutColors = [ + colord({ r: 120, g: 255, b: 71 }), // Original color + colord({ r: 130, g: 255, b: 85 }), // Slightly lighter + colord({ r: 110, g: 245, b: 65 }), // Slightly darker + colord({ r: 125, g: 255, b: 75 }), // Warmer tint + colord({ r: 115, g: 250, b: 68 }) // Cooler tint + ]; + private water = colord({ r: 75, g: 142, b: 190 }); + private shorelineWater = colord({ r: 100, g: 143, b: 255 }); private territoryColors: Colord[] = [ - colord({r: 230, g: 100, b: 100}), // Bright Red - colord({r: 100, g: 180, b: 230}), // Sky Blue - colord({r: 230, g: 180, b: 80}), // Golden Yellow - colord({r: 180, g: 100, b: 230}), // Purple - colord({r: 80, g: 200, b: 120}), // Emerald Green - colord({r: 230, g: 130, b: 180}), // Pink - colord({r: 100, g: 160, b: 80}), // Olive Green - colord({r: 230, g: 150, b: 100}), // Peach - colord({r: 80, g: 130, b: 190}), // Navy Blue - colord({r: 210, g: 210, b: 100}), // Lime Yellow - colord({r: 190, g: 100, b: 130}), // Maroon - colord({r: 100, g: 210, b: 210}), // Turquoise - colord({r: 210, g: 140, b: 80}), // Light Orange - colord({r: 150, g: 110, b: 190}), // Lavender - colord({r: 180, g: 210, b: 120}), // Light Green - colord({r: 210, g: 100, b: 160}), // Hot Pink - colord({r: 100, g: 140, b: 110}), // Sea Green - colord({r: 230, g: 180, b: 180}), // Light Pink - colord({r: 120, g: 120, b: 190}), // Periwinkle - colord({r: 190, g: 170, b: 100}), // Sand - colord({r: 100, g: 180, b: 160}), // Aquamarine - colord({r: 210, g: 160, b: 200}), // Orchid - colord({r: 170, g: 190, b: 100}), // Yellow Green - colord({r: 100, g: 130, b: 150}), // Steel Blue - colord({r: 230, g: 140, b: 140}), // Salmon - colord({r: 140, g: 180, b: 220}), // Light Blue - colord({r: 200, g: 160, b: 110}), // Tan - colord({r: 180, g: 130, b: 180}), // Plum - colord({r: 130, g: 200, b: 130}), // Light Sea Green - colord({r: 220, g: 120, b: 120}), // Coral - colord({r: 120, g: 160, b: 200}), // Cornflower Blue - colord({r: 200, g: 200, b: 140}), // Khaki - colord({r: 160, g: 120, b: 160}), // Purple Gray - colord({r: 140, g: 180, b: 140}), // Dark Sea Green - colord({r: 200, g: 130, b: 110}), // Dark Salmon - colord({r: 130, g: 170, b: 190}), // Cadet Blue - colord({r: 190, g: 180, b: 160}), // Tan Gray - colord({r: 170, g: 140, b: 190}), // Medium Purple - colord({r: 160, g: 190, b: 160}), // Pale Green - colord({r: 190, g: 150, b: 130}), // Rosy Brown - colord({r: 140, g: 150, b: 180}), // Light Slate Gray - colord({r: 180, g: 170, b: 140}), // Dark Khaki - colord({r: 150, g: 130, b: 150}), // Thistle - colord({r: 170, g: 190, b: 180}), // Pale Blue Green - colord({r: 190, g: 140, b: 150}), // Puce - colord({r: 130, g: 180, b: 170}), // Medium Aquamarine - colord({r: 180, g: 160, b: 180}), // Mauve - colord({r: 160, g: 180, b: 140}), // Dark Olive Green - colord({r: 170, g: 150, b: 170}), // Dusty Rose - colord({r: 100, g: 180, b: 230}), // Sky Blue - colord({r: 230, g: 180, b: 80}), // Golden Yellow - colord({r: 180, g: 100, b: 230}), // Purple - colord({r: 80, g: 200, b: 120}), // Emerald Green - colord({r: 230, g: 130, b: 180}), // Pink - colord({r: 100, g: 160, b: 80}), // Olive Green - colord({r: 230, g: 150, b: 100}), // Peach - colord({r: 80, g: 130, b: 190}), // Navy Blue - colord({r: 210, g: 210, b: 100}), // Lime Yellow - colord({r: 190, g: 100, b: 130}), // Maroon - colord({r: 100, g: 210, b: 210}), // Turquoise - colord({r: 210, g: 140, b: 80}), // Light Orange - colord({r: 150, g: 110, b: 190}), // Lavender - colord({r: 180, g: 210, b: 120}), // Light Green - colord({r: 210, g: 100, b: 160}), // Hot Pink - colord({r: 100, g: 140, b: 110}), // Sea Green - colord({r: 230, g: 180, b: 180}), // Light Pink - colord({r: 120, g: 120, b: 190}), // Periwinkle - colord({r: 190, g: 170, b: 100}), // Sand - colord({r: 100, g: 180, b: 160}), // Aquamarine - colord({r: 210, g: 160, b: 200}), // Orchid - colord({r: 170, g: 190, b: 100}), // Yellow Green - colord({r: 100, g: 130, b: 150}), // Steel Blue - colord({r: 230, g: 140, b: 140}), // Salmon - colord({r: 140, g: 180, b: 220}), // Light Blue - colord({r: 200, g: 160, b: 110}), // Tan - colord({r: 180, g: 130, b: 180}), // Plum - colord({r: 130, g: 200, b: 130}), // Light Sea Green - colord({r: 220, g: 120, b: 120}), // Coral - colord({r: 120, g: 160, b: 200}), // Cornflower Blue - colord({r: 200, g: 200, b: 140}), // Khaki - colord({r: 160, g: 120, b: 160}), // Purple Gray - colord({r: 140, g: 180, b: 140}), // Dark Sea Green - colord({r: 200, g: 130, b: 110}), // Dark Salmon - colord({r: 130, g: 170, b: 190}), // Cadet Blue - colord({r: 190, g: 180, b: 160}), // Tan Gray - colord({r: 170, g: 140, b: 190}), // Medium Purple - colord({r: 160, g: 190, b: 160}), // Pale Green - colord({r: 190, g: 150, b: 130}), // Rosy Brown - colord({r: 140, g: 150, b: 180}), // Light Slate Gray - colord({r: 180, g: 170, b: 140}), // Dark Khaki - colord({r: 150, g: 130, b: 150}), // Thistle - colord({r: 170, g: 190, b: 180}), // Pale Blue Green - colord({r: 190, g: 140, b: 150}), // Puce - colord({r: 130, g: 180, b: 170}), // Medium Aquamarine - colord({r: 180, g: 160, b: 180}), // Mauve - colord({r: 160, g: 180, b: 140}), // Dark Olive Green - colord({r: 170, g: 150, b: 170}) // Dusty Rose + colord({ r: 230, g: 100, b: 100 }), // Bright Red + colord({ r: 100, g: 180, b: 230 }), // Sky Blue + colord({ r: 230, g: 180, b: 80 }), // Golden Yellow + colord({ r: 180, g: 100, b: 230 }), // Purple + colord({ r: 80, g: 200, b: 120 }), // Emerald Green + colord({ r: 230, g: 130, b: 180 }), // Pink + colord({ r: 100, g: 160, b: 80 }), // Olive Green + colord({ r: 230, g: 150, b: 100 }), // Peach + colord({ r: 80, g: 130, b: 190 }), // Navy Blue + colord({ r: 210, g: 210, b: 100 }), // Lime Yellow + colord({ r: 190, g: 100, b: 130 }), // Maroon + colord({ r: 100, g: 210, b: 210 }), // Turquoise + colord({ r: 210, g: 140, b: 80 }), // Light Orange + colord({ r: 150, g: 110, b: 190 }), // Lavender + colord({ r: 180, g: 210, b: 120 }), // Light Green + colord({ r: 210, g: 100, b: 160 }), // Hot Pink + colord({ r: 100, g: 140, b: 110 }), // Sea Green + colord({ r: 230, g: 180, b: 180 }), // Light Pink + colord({ r: 120, g: 120, b: 190 }), // Periwinkle + colord({ r: 190, g: 170, b: 100 }), // Sand + colord({ r: 100, g: 180, b: 160 }), // Aquamarine + colord({ r: 210, g: 160, b: 200 }), // Orchid + colord({ r: 170, g: 190, b: 100 }), // Yellow Green + colord({ r: 100, g: 130, b: 150 }), // Steel Blue + colord({ r: 230, g: 140, b: 140 }), // Salmon + colord({ r: 140, g: 180, b: 220 }), // Light Blue + colord({ r: 200, g: 160, b: 110 }), // Tan + colord({ r: 180, g: 130, b: 180 }), // Plum + colord({ r: 130, g: 200, b: 130 }), // Light Sea Green + colord({ r: 220, g: 120, b: 120 }), // Coral + colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue + colord({ r: 200, g: 200, b: 140 }), // Khaki + colord({ r: 160, g: 120, b: 160 }), // Purple Gray + colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green + colord({ r: 200, g: 130, b: 110 }), // Dark Salmon + colord({ r: 130, g: 170, b: 190 }), // Cadet Blue + colord({ r: 190, g: 180, b: 160 }), // Tan Gray + colord({ r: 170, g: 140, b: 190 }), // Medium Purple + colord({ r: 160, g: 190, b: 160 }), // Pale Green + colord({ r: 190, g: 150, b: 130 }), // Rosy Brown + colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray + colord({ r: 180, g: 170, b: 140 }), // Dark Khaki + colord({ r: 150, g: 130, b: 150 }), // Thistle + colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green + colord({ r: 190, g: 140, b: 150 }), // Puce + colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine + colord({ r: 180, g: 160, b: 180 }), // Mauve + colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green + colord({ r: 170, g: 150, b: 170 }), // Dusty Rose + colord({ r: 100, g: 180, b: 230 }), // Sky Blue + colord({ r: 230, g: 180, b: 80 }), // Golden Yellow + colord({ r: 180, g: 100, b: 230 }), // Purple + colord({ r: 80, g: 200, b: 120 }), // Emerald Green + colord({ r: 230, g: 130, b: 180 }), // Pink + colord({ r: 100, g: 160, b: 80 }), // Olive Green + colord({ r: 230, g: 150, b: 100 }), // Peach + colord({ r: 80, g: 130, b: 190 }), // Navy Blue + colord({ r: 210, g: 210, b: 100 }), // Lime Yellow + colord({ r: 190, g: 100, b: 130 }), // Maroon + colord({ r: 100, g: 210, b: 210 }), // Turquoise + colord({ r: 210, g: 140, b: 80 }), // Light Orange + colord({ r: 150, g: 110, b: 190 }), // Lavender + colord({ r: 180, g: 210, b: 120 }), // Light Green + colord({ r: 210, g: 100, b: 160 }), // Hot Pink + colord({ r: 100, g: 140, b: 110 }), // Sea Green + colord({ r: 230, g: 180, b: 180 }), // Light Pink + colord({ r: 120, g: 120, b: 190 }), // Periwinkle + colord({ r: 190, g: 170, b: 100 }), // Sand + colord({ r: 100, g: 180, b: 160 }), // Aquamarine + colord({ r: 210, g: 160, b: 200 }), // Orchid + colord({ r: 170, g: 190, b: 100 }), // Yellow Green + colord({ r: 100, g: 130, b: 150 }), // Steel Blue + colord({ r: 230, g: 140, b: 140 }), // Salmon + colord({ r: 140, g: 180, b: 220 }), // Light Blue + colord({ r: 200, g: 160, b: 110 }), // Tan + colord({ r: 180, g: 130, b: 180 }), // Plum + colord({ r: 130, g: 200, b: 130 }), // Light Sea Green + colord({ r: 220, g: 120, b: 120 }), // Coral + colord({ r: 120, g: 160, b: 200 }), // Cornflower Blue + colord({ r: 200, g: 200, b: 140 }), // Khaki + colord({ r: 160, g: 120, b: 160 }), // Purple Gray + colord({ r: 140, g: 180, b: 140 }), // Dark Sea Green + colord({ r: 200, g: 130, b: 110 }), // Dark Salmon + colord({ r: 130, g: 170, b: 190 }), // Cadet Blue + colord({ r: 190, g: 180, b: 160 }), // Tan Gray + colord({ r: 170, g: 140, b: 190 }), // Medium Purple + colord({ r: 160, g: 190, b: 160 }), // Pale Green + colord({ r: 190, g: 150, b: 130 }), // Rosy Brown + colord({ r: 140, g: 150, b: 180 }), // Light Slate Gray + colord({ r: 180, g: 170, b: 140 }), // Dark Khaki + colord({ r: 150, g: 130, b: 150 }), // Thistle + colord({ r: 170, g: 190, b: 180 }), // Pale Blue Green + colord({ r: 190, g: 140, b: 150 }), // Puce + colord({ r: 130, g: 180, b: 170 }), // Medium Aquamarine + colord({ r: 180, g: 160, b: 180 }), // Mauve + colord({ r: 160, g: 180, b: 140 }), // Dark Olive Green + colord({ r: 170, g: 150, b: 170 }) // Dusty Rose ]; playerInfoColor(id: PlayerID): Colord { - return colord({r: 50, g: 50, b: 50}) + return colord({ r: 50, g: 50, b: 50 }) } territoryColor(playerInfo: PlayerInfo): Colord { @@ -186,6 +192,10 @@ export const pastelTheme = new class implements Theme { return this.background; } + falloutColor(): Colord { + return this.rand.randElement(this.falloutColors) + } + font(): string { return "Overpass, sans-serif"; } diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 9e095ffaa..ce24c64ac 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -83,6 +83,9 @@ export class NukeExecution implements Execution { mp.removeTroops(2 * ratio[mp.id()]) others.add(mp) } + if (tile.isLand()) { + this.mg.addFallout(tile) + } } for (const other of others) { const alliance = this.player.allianceWith(other) diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index b46bf0dae..4f111a51f 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -174,6 +174,7 @@ export interface Tile extends SearchNode { defenseBonuses(): DefenseBonus[] // defense bonus against this player defenseBonus(player: Player): number + hasFallout(): boolean } export interface Unit { @@ -314,6 +315,7 @@ export interface MutableGame extends Game { units(...types: UnitType[]): MutableUnit[] addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus removeTileDefenseBonus(bonus: DefenseBonus): void + addFallout(tile: Tile) } export class TileEvent implements GameEvent { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index fd80ca762..61f05631c 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -65,6 +65,15 @@ export class GameImpl implements MutableGame { }) } + addFallout(tile: Tile) { + const ti = tile as TileImpl + if (tile.hasOwner()) { + throw Error(`cannot set fallout, tile ${tile} has owner`) + } + ti._hasFallout = true + this.eventBus.emit(new TileEvent(tile)) + } + addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus { const df = { unit: unit, tile: tile, amount: amount }; (tile as TileImpl)._defenseBonuses.push(df) @@ -322,6 +331,7 @@ export class GameImpl implements MutableGame { tileImpl._owner = owner owner._tiles.set(tile.cell().toString(), tile) this.updateBorders(tile) + tileImpl._hasFallout = false this.eventBus.emit(new TileEvent(tile)) } diff --git a/src/core/game/TileImpl.ts b/src/core/game/TileImpl.ts index b88923b22..65b3ec26a 100644 --- a/src/core/game/TileImpl.ts +++ b/src/core/game/TileImpl.ts @@ -13,6 +13,8 @@ export class TileImpl implements Tile { public _defenseBonuses: DefenseBonus[] = [] + public _hasFallout = false + constructor( private readonly gs: GameImpl, public _owner: PlayerImpl | TerraNulliusImpl, @@ -20,6 +22,10 @@ export class TileImpl implements Tile { private readonly _terrain: TerrainTileImpl ) { } + hasFallout(): boolean { + return this._hasFallout + } + terrainType(): TerrainType { return this._terrain.type } From 39a54b9a6785db85d70f0443cd9f414bb94af17d Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 4 Dec 2024 13:55:16 -0800 Subject: [PATCH 2/6] add city icons --- TODO.txt | 4 +-- resources/images/BuildingIcon.png | Bin 0 -> 129 bytes resources/images/CityIconWhite.svg | 56 +++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 resources/images/BuildingIcon.png create mode 100644 resources/images/CityIconWhite.svg diff --git a/TODO.txt b/TODO.txt index 547d97b27..d91f4ff2e 100644 --- a/TODO.txt +++ b/TODO.txt @@ -207,11 +207,11 @@ * use mini A* for all pathfinding DONE 12/3/2024 * bugfix: gameStop not found error DONE 12/3/2024 * log stack traces & display them on screen DONE 12/3/2024 -* add radiation from nuke +* add radiation from nuke DONE 12/4/2024 +* add cities * record and replay games for debugging purposes * bugfix: destroyers can't find path to dst and freeze * record single player game stats -* add cities * create behavior tests * create perf test * create alternate view to show friendly & enemy units diff --git a/resources/images/BuildingIcon.png b/resources/images/BuildingIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..a690bfa9eb131dde00b189cf11823d0ee1f623d5 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VjKx9jP7LeL$-D$|tUX;ELo|Yu zQyf_TPu$UCU^eIAfddS|3_{Ln#tFeKIc5=Z%Q#%sLqubEVg+XjAB;K1XuxTivtef^ aBg2|rUQQ{KkiS4<7(8A5T-G@yGywpi*CKlW literal 0 HcmV?d00001 diff --git a/resources/images/CityIconWhite.svg b/resources/images/CityIconWhite.svg new file mode 100644 index 000000000..0c55a2d06 --- /dev/null +++ b/resources/images/CityIconWhite.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + From e068a9afc7aac7123420f14961b7e012e117e7b6 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 4 Dec 2024 16:27:07 -0800 Subject: [PATCH 3/6] implement cities --- .../images/{BuildingIcon.png => CityIcon.png} | Bin src/client/graphics/layers/StructureLayer.ts | 6 +++ .../graphics/layers/radial/BuildMenu.ts | 4 +- src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 11 +++- src/core/execution/CityExecution.ts | 48 ++++++++++++++++++ src/core/execution/ExecutionManager.ts | 3 ++ src/core/game/Game.ts | 3 +- src/core/game/PlayerImpl.ts | 2 + 9 files changed, 75 insertions(+), 3 deletions(-) rename resources/images/{BuildingIcon.png => CityIcon.png} (100%) create mode 100644 src/core/execution/CityExecution.ts diff --git a/resources/images/BuildingIcon.png b/resources/images/CityIcon.png similarity index 100% rename from resources/images/BuildingIcon.png rename to resources/images/CityIcon.png diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index f5e2e5674..49d84176d 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -8,6 +8,7 @@ import { EventBus } from "../../../core/EventBus"; import anchorIcon from '../../../../resources/images/AnchorIcon.png'; import missileSiloIcon from '../../../../resources/images/MissileSiloUnit.png'; import shieldIcon from '../../../../resources/images/ShieldIcon.png'; +import cityIcon from '../../../../resources/images/CityIcon.png'; interface UnitRenderConfig { icon: string; @@ -38,6 +39,11 @@ export class StructureLayer implements Layer { icon: shieldIcon, borderRadius: 8, territoryRadius: 6 + }, + [UnitType.City]: { + icon: cityIcon, + borderRadius: 8, + territoryRadius: 6 } }; diff --git a/src/client/graphics/layers/radial/BuildMenu.ts b/src/client/graphics/layers/radial/BuildMenu.ts index f98556e74..b57f5c343 100644 --- a/src/client/graphics/layers/radial/BuildMenu.ts +++ b/src/client/graphics/layers/radial/BuildMenu.ts @@ -11,6 +11,7 @@ import missileSiloIcon from '../../../../../resources/images/MissileSiloIconWhit import goldCoinIcon from '../../../../../resources/images/GoldCoinIcon.svg'; import portIcon from '../../../../../resources/images/PortIcon.svg'; import shieldIcon from '../../../../../resources/images/ShieldIconWhite.svg'; +import cityIcon from '../../../../../resources/images/CityIconWhite.svg'; import { renderNumber } from '../../Utils'; import { ContextMenuEvent } from '../../../InputHandler'; @@ -27,7 +28,8 @@ const buildTable: BuildItemDisplay[][] = [ { unitType: UnitType.Battleship, icon: battleshipIcon }, { unitType: UnitType.Port, icon: portIcon }, { unitType: UnitType.MissileSilo, icon: missileSiloIcon }, - { unitType: UnitType.DefensePost, icon: shieldIcon } + { unitType: UnitType.DefensePost, icon: shieldIcon }, + { unitType: UnitType.City, icon: cityIcon } ] ]; diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 277860ce5..5233148fa 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -46,6 +46,7 @@ export interface Config { } attackAmount(attacker: Player, defender: Player | TerraNullius): number maxPopulation(player: Player): number + cityPopulationIncrease(): number boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number boatMaxDistance(): number boatMaxNumber(): number diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 45fb05c14..67dc62d99 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -8,6 +8,10 @@ import { pastelTheme } from "./PastelTheme"; export class DefaultConfig implements Config { + cityPopulationIncrease(): number { + return 250_000 + } + falloutDefenseModifier(): number { return 2 } @@ -80,6 +84,11 @@ export class DefaultConfig implements Config { cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 100_000, territoryBound: true } + case UnitType.City: + return { + cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 250_000, + territoryBound: true + } default: assertNever(type) } @@ -217,7 +226,7 @@ export class DefaultConfig implements Config { if (player.type() == PlayerType.Bot) { return maxPop } - return maxPop * 2 + return maxPop * 2 + player.units(UnitType.City).length * this.cityPopulationIncrease() } populationIncreaseRate(player: Player): number { diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts new file mode 100644 index 000000000..e9046f0dd --- /dev/null +++ b/src/core/execution/CityExecution.ts @@ -0,0 +1,48 @@ +import { Cell, DefenseBonus, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; +import { bfs, dist } from "../Util"; + +export class CityExecution implements Execution { + + private player: MutablePlayer + private mg: MutableGame + private city: MutableUnit + private tile: Tile + private active: boolean = true + + constructor(private ownerId: PlayerID, private cell: Cell) { } + + init(mg: MutableGame, ticks: number): void { + this.mg = mg + this.tile = mg.tile(this.cell) + this.player = mg.player(this.ownerId) + } + + tick(ticks: number): void { + if (this.city == null) { + const spawnTile = this.player.canBuild(UnitType.City, this.tile) + if (spawnTile == false) { + console.warn('cannot build Defense Post') + this.active = false + return + } + this.city = this.player.buildUnit(UnitType.City, 0, spawnTile) + } + if (!this.city.isActive()) { + this.active = false + 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/execution/ExecutionManager.ts b/src/core/execution/ExecutionManager.ts index 4ba1dbf3b..20c3a16b9 100644 --- a/src/core/execution/ExecutionManager.ts +++ b/src/core/execution/ExecutionManager.ts @@ -23,6 +23,7 @@ import { BattleshipExecution } from "./BattleshipExecution"; import { PathFinder } from "../pathfinding/PathFinding"; import { WorkerClient } from "../worker/WorkerClient"; import { DefensePostExecution } from "./DefensePostExecution"; +import { CityExecution } from "./CityExecution"; @@ -100,6 +101,8 @@ export class Executor { return new MissileSiloExecution(intent.player, new Cell(intent.x, intent.y)) case UnitType.DefensePost: return new DefensePostExecution(intent.player, new Cell(intent.x, intent.y)) + case UnitType.City: + return new CityExecution(intent.player, new Cell(intent.x, intent.y)) default: throw Error(`unit type ${intent.unit} not supported`) } diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index 4f111a51f..d940ab40f 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -39,7 +39,8 @@ export enum UnitType { HydrogenBomb = "Hydrogen Bomb", TradeShip = "Trade Ship", MissileSilo = "Missile Silo", - DefensePost = "Defense Post" + DefensePost = "Defense Post", + City = "City" } export class Nation { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 8c2113a0a..c8507e386 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -372,6 +372,8 @@ export class PlayerImpl implements MutablePlayer { return this.transportShipSpawn(targetTile) case UnitType.TradeShip: return this.tradeShipSpawn(targetTile) + case UnitType.City: + return this.landBasedStructureSpawn(targetTile) default: assertNever(unitType) } From 22b877e85c7d7d14a16535c756bc49615f4016e7 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 4 Dec 2024 16:46:27 -0800 Subject: [PATCH 4/6] in progress --- TODO.txt | 6 +- src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 127 +++++++++++++----------- src/core/configuration/DevConfig.ts | 5 +- 4 files changed, 78 insertions(+), 61 deletions(-) diff --git a/TODO.txt b/TODO.txt index d91f4ff2e..cd48c2bcd 100644 --- a/TODO.txt +++ b/TODO.txt @@ -208,7 +208,8 @@ * bugfix: gameStop not found error DONE 12/3/2024 * log stack traces & display them on screen DONE 12/3/2024 * add radiation from nuke DONE 12/4/2024 -* add cities +* add cities DONE 12/4/2024 +* max price for units * record and replay games for debugging purposes * bugfix: destroyers can't find path to dst and freeze * record single player game stats @@ -232,4 +233,5 @@ * improve front page (make map larger?) * REFACTOR: give terranullius an ID, game.player() returns terranullius -* REFACTOR: ocean is considered TerraNullius ? \ No newline at end of file +* REFACTOR: ocean is considered TerraNullius ? + diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 5233148fa..5cb85ad24 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -64,6 +64,7 @@ export interface Config { defensePostRange(): number defensePostDefenseBonus(): number falloutDefenseModifier(): number + maxUnitCost(): number } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 67dc62d99..c77d592c6 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -8,6 +8,10 @@ import { pastelTheme } from "./PastelTheme"; export class DefaultConfig implements Config { + maxUnitCost(): number { + return 99_999_999 + } + cityPopulationIncrease(): number { return 250_000 } @@ -32,66 +36,73 @@ export class DefaultConfig implements Config { tradeShipSpawnRate(): number { return 500 } + unitInfo(type: UnitType): UnitInfo { - switch (type) { - case UnitType.TransportShip: - return { - cost: () => 0, - territoryBound: false - } - case UnitType.Destroyer: - return { - 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) => 0, - territoryBound: false - } - case UnitType.Port: - return { - cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 250_000, - territoryBound: true - } - case UnitType.AtomBomb: - return { - cost: () => 1_000_000, - territoryBound: false - } - case UnitType.HydrogenBomb: - return { - cost: () => 5_000_000, - territoryBound: false - } - case UnitType.TradeShip: - return { - cost: () => 0, - territoryBound: false - } - case UnitType.MissileSilo: - return { - cost: () => 1_000_000, - territoryBound: true - } - case UnitType.DefensePost: - return { - cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 100_000, - territoryBound: true - } - case UnitType.City: - return { - cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 250_000, - territoryBound: true - } - default: - assertNever(type) + const fn = () => { + switch (type) { + case UnitType.TransportShip: + return { + cost: () => 0, + territoryBound: false + } + case UnitType.Destroyer: + return { + 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) => 0, + territoryBound: false + } + case UnitType.Port: + return { + cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 250_000, + territoryBound: true + } + case UnitType.AtomBomb: + return { + cost: () => 1_000_000, + territoryBound: false + } + case UnitType.HydrogenBomb: + return { + cost: () => 5_000_000, + territoryBound: false + } + case UnitType.TradeShip: + return { + cost: () => 0, + territoryBound: false + } + case UnitType.MissileSilo: + return { + cost: () => 1_000_000, + territoryBound: true + } + case UnitType.DefensePost: + return { + cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 100_000, + territoryBound: true + } + case UnitType.City: + return { + cost: (p: Player) => Math.pow(2, p.units(UnitType.Port).length) * 250_000, + territoryBound: true + } + default: + assertNever(type) + } } + const ui = fn() + const oldCost = ui.cost + ui.cost = (p: Player) => Math.min(this.maxUnitCost(), oldCost(p)) + return ui } defaultDonationAmount(sender: Player): number { return Math.floor(sender.troops() / 3) diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index e5dc3bdda..c5f4be13a 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -5,9 +5,12 @@ export const devConfig = new class extends DefaultConfig { unitInfo(type: UnitType): UnitInfo { const info = super.unitInfo(type) const oldCost = info.cost - info.cost = (p: Player) => oldCost(p) / 100000 + info.cost = (p: Player) => oldCost(p) / 1000 return info } + maxUnitCost(): number { + return 10000 + } percentageTilesOwnedToWin(): number { return 95 From 731536931e6fc474f3c07c5f3a134796329beb44 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 4 Dec 2024 18:52:41 -0800 Subject: [PATCH 5/6] have game server store game --- TODO.txt | 10 + package-lock.json | 372 +++++++++++++++++++++++- package.json | 1 + src/core/configuration/Config.ts | 1 + src/core/configuration/DefaultConfig.ts | 3 + src/server/GameManager.ts | 4 +- src/server/GameServer.ts | 22 +- 7 files changed, 403 insertions(+), 10 deletions(-) diff --git a/TODO.txt b/TODO.txt index cd48c2bcd..599c21bfb 100644 --- a/TODO.txt +++ b/TODO.txt @@ -211,8 +211,18 @@ * add cities DONE 12/4/2024 * max price for units * record and replay games for debugging purposes +* add bug report button in game * bugfix: destroyers can't find path to dst and freeze * record single player game stats +* add radiation from nuke +* add cities +* nuking an enemy and accidentally destroying a trade ship shouldn't break the alliance and make you a traitor +* you should get a notification and a reward (some money) for eliminating an enemy (perhaps take wtvr gold the enemy had) +* emojis should be displayed on top of your name not under it +* the notification for a successful trade should be shorter, example: " 70k Gold from trade with "X" " +* countries don't actually spawn with some randomness, it's always the same exact spawn +* allow longer names and allow them to be displayed in the Rank UI not be cut (many are cut for now, even for countries) +* clicking on a player's name in the rank UI should teleport you to him (pretty useful to know who's who and to locate small nations) * create behavior tests * create perf test * create alternate view to show friendly & enemy units diff --git a/package-lock.json b/package-lock.json index e510a1acd..fbbc30ad3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "name": "openfront-client", "dependencies": { "@datastructures-js/priority-queue": "^6.3.1", + "@google-cloud/storage": "^7.14.0", "@types/dompurify": "^3.0.5", "@types/express": "^4.17.21", "@types/google-protobuf": "^3.15.12", @@ -2349,6 +2350,93 @@ "node": ">=18" } }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "license": "Apache-2.0", + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator/node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.14.0.tgz", + "integrity": "sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ==", + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3568,6 +3656,15 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3661,6 +3758,12 @@ "@types/node": "*" } }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" + }, "node_modules/@types/chai": { "version": "4.3.20", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", @@ -4208,6 +4311,18 @@ "safe-buffer": "~5.1.1" } }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "license": "MIT", + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, "node_modules/@types/retry": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", @@ -4287,6 +4402,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4855,6 +4976,30 @@ "dev": true, "license": "MIT" }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/async-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5703,6 +5848,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -6610,6 +6767,15 @@ "robust-predicates": "^3.0.2" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6780,6 +6946,32 @@ "tslib": "^2.0.3" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -6872,6 +7064,15 @@ "iconv-lite": "^0.6.2" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -7309,6 +7510,28 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/fast-xml-parser": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.0.tgz", + "integrity": "sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -7550,6 +7773,41 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", + "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/form-data/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -8041,7 +8299,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", - "dev": true, "funding": [ { "type": "github", @@ -10807,7 +11064,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -10861,7 +11117,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -11912,6 +12167,20 @@ "node": ">= 4" } }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -12518,6 +12787,21 @@ "node": ">= 0.8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "license": "MIT", + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -12648,6 +12932,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "license": "MIT" + }, "node_modules/strtok3": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", @@ -12665,6 +12955,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "license": "MIT" + }, "node_modules/style-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", @@ -12774,6 +13070,74 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/terser": { "version": "5.36.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", @@ -14198,7 +14562,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -14394,7 +14757,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index cdafbfd87..ca13da352 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ }, "dependencies": { "@datastructures-js/priority-queue": "^6.3.1", + "@google-cloud/storage": "^7.14.0", "@types/dompurify": "^3.0.5", "@types/express": "^4.17.21", "@types/google-protobuf": "^3.15.12", diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 5cb85ad24..a5fd68024 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -65,6 +65,7 @@ export interface Config { defensePostDefenseBonus(): number falloutDefenseModifier(): number maxUnitCost(): number + gameStorageBucketName(): string } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index c77d592c6..dbe8eb1b3 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -20,6 +20,9 @@ export class DefaultConfig implements Config { return 2 } + gameStorageBucketName(): string { + return "openfront-games" + } defensePostRange(): number { return 30 } diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts index 1b8588cad..ea8ee1c1e 100644 --- a/src/server/GameManager.ts +++ b/src/server/GameManager.ts @@ -74,9 +74,7 @@ export class GameManager { active.filter(g => !g.hasStarted() && g.isPublic).forEach(g => { g.start() }) - finished.forEach(g => { - g.endGame() - }) + finished.map(g => g.endGame()); // Fire and forget this.games = [...lobbies, ...active] } } diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 777bbbe84..d39151a37 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -3,7 +3,9 @@ import { Config } from "../core/configuration/Config"; import { Client } from "./Client"; import WebSocket from 'ws'; import { slog } from "./StructuredLog"; +import { Storage } from '@google-cloud/storage'; +const storage = new Storage(); export enum GamePhase { Lobby = 'LOBBY', @@ -14,7 +16,7 @@ export enum GamePhase { export class GameServer { - private maxGameDuration = 60 * 60 * 1000 // 1 hour + private maxGameDuration = 2 * 60 * 60 * 1000 // 2 hours private turns: Turn[] = [] private intents: Intent[] = [] @@ -135,7 +137,7 @@ export class GameServer { }) } - endGame() { + async endGame() { // Close all WebSocket connections clearInterval(this.endTurnIntervalID); this.clients.forEach(client => { @@ -144,6 +146,22 @@ export class GameServer { client.ws.close(); } }); + try { + if (this.turns.length > 100 && this.clients.length > 0) { + const bucket = storage.bucket(this.config.gameStorageBucketName()); + const file = bucket.file(this.id); + const game = { + id: this.id, + date: new Date().toISOString().split('T')[0], + turns: this.turns + } + await file.save(JSON.stringify(game), { + contentType: 'application/json' + }); + } + } catch (error) { + console.log('error writing game to gcs: ' + error) + } } phase(): GamePhase { From 0aac25dc51997367bbcc1a3f6e96afd2b21d6e22 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 4 Dec 2024 19:05:16 -0800 Subject: [PATCH 6/6] update TODO --- TODO.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TODO.txt b/TODO.txt index 599c21bfb..56a1e10dc 100644 --- a/TODO.txt +++ b/TODO.txt @@ -210,12 +210,12 @@ * add radiation from nuke DONE 12/4/2024 * add cities DONE 12/4/2024 * max price for units +* when player dies, don't remove atom bombs * record and replay games for debugging purposes * add bug report button in game * bugfix: destroyers can't find path to dst and freeze * record single player game stats -* add radiation from nuke -* add cities +* stop requesting lobby when playing game * nuking an enemy and accidentally destroying a trade ship shouldn't break the alliance and make you a traitor * you should get a notification and a reward (some money) for eliminating an enemy (perhaps take wtvr gold the enemy had) * emojis should be displayed on top of your name not under it