From 1d0732d3d9ee04e91b93bf1cf2bbf852866c61a4 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sat, 10 May 2025 11:40:47 -0700 Subject: [PATCH 1/3] Refactor UnitSpecific info => AllUnitParams type union (#701) ## Description: By using a type union we get better type safety, enforcing each unit type have the appropriate params when initializing ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --------- Co-authored-by: evan --- src/client/ClientGameRunner.ts | 1 + src/core/execution/CityExecution.ts | 2 +- src/core/execution/ConstructionExecution.ts | 2 +- src/core/execution/DefensePostExecution.ts | 2 +- src/core/execution/MIRVExecution.ts | 2 +- src/core/execution/MissileSiloExecution.ts | 2 +- src/core/execution/NukeExecution.ts | 2 +- src/core/execution/PortExecution.ts | 2 +- src/core/execution/SAMLauncherExecution.ts | 2 +- src/core/execution/SAMMissileExecution.ts | 2 +- src/core/execution/ShellExecution.ts | 2 +- src/core/execution/TradeShipExecution.ts | 2 +- src/core/execution/TransportShipExecution.ts | 8 +-- src/core/execution/WarshipExecution.ts | 2 +- src/core/game/Game.ts | 64 +++++++++++++++----- src/core/game/PlayerImpl.ts | 14 ++--- src/core/game/UnitImpl.ts | 20 +++--- tests/SAM.test.ts | 30 ++++++--- tests/Warship.test.ts | 12 ++-- 19 files changed, 111 insertions(+), 62 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 374cc8e86..f782ee0b9 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -239,6 +239,7 @@ export class ClientGameRunner { this.lobby.gameStartInfo.gameID, this.lobby.clientID, ); + console.error(gu.stack); this.stop(true); return; } diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts index 5bf2ca7e4..289a64957 100644 --- a/src/core/execution/CityExecution.ts +++ b/src/core/execution/CityExecution.ts @@ -38,7 +38,7 @@ export class CityExecution implements Execution { this.active = false; return; } - this.city = this.player.buildUnit(UnitType.City, 0, spawnTile); + this.city = this.player.buildUnit(UnitType.City, spawnTile, {}); } if (!this.city.isActive()) { this.active = false; diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts index c5e73f1f2..36242a4b4 100644 --- a/src/core/execution/ConstructionExecution.ts +++ b/src/core/execution/ConstructionExecution.ts @@ -60,8 +60,8 @@ export class ConstructionExecution implements Execution { } this.construction = this.player.buildUnit( UnitType.Construction, - 0, spawnTile, + {}, ); this.cost = this.mg.unitInfo(this.constructionType).cost(this.player); this.player.removeGold(this.cost); diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index 413cf93b4..8884f86d3 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -65,7 +65,7 @@ export class DefensePostExecution implements Execution { this.active = false; return; } - this.post = this.player.buildUnit(UnitType.DefensePost, 0, spawnTile); + this.post = this.player.buildUnit(UnitType.DefensePost, spawnTile, {}); } if (!this.post.isActive()) { this.active = false; diff --git a/src/core/execution/MIRVExecution.ts b/src/core/execution/MIRVExecution.ts index a752adfef..66d38f8e1 100644 --- a/src/core/execution/MIRVExecution.ts +++ b/src/core/execution/MIRVExecution.ts @@ -70,7 +70,7 @@ export class MirvExecution implements Execution { this.active = false; return; } - this.nuke = this.player.buildUnit(UnitType.MIRV, 0, spawn); + this.nuke = this.player.buildUnit(UnitType.MIRV, spawn, {}); const x = Math.floor( (this.mg.x(this.dst) + this.mg.x(this.mg.x(this.nuke.tile()))) / 2, ); diff --git a/src/core/execution/MissileSiloExecution.ts b/src/core/execution/MissileSiloExecution.ts index fd9bf2111..b13eaadef 100644 --- a/src/core/execution/MissileSiloExecution.ts +++ b/src/core/execution/MissileSiloExecution.ts @@ -41,7 +41,7 @@ export class MissileSiloExecution implements Execution { this.active = false; return; } - this.silo = this.player.buildUnit(UnitType.MissileSilo, 0, spawn, { + this.silo = this.player.buildUnit(UnitType.MissileSilo, spawn, { cooldownDuration: this.mg.config().SiloCooldown(), }); diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts index 8795e349f..666f2f9c7 100644 --- a/src/core/execution/NukeExecution.ts +++ b/src/core/execution/NukeExecution.ts @@ -95,7 +95,7 @@ export class NukeExecution implements Execution { this.active = false; return; } - this.nuke = this.player.buildUnit(this.type, 0, spawn, { + this.nuke = this.player.buildUnit(this.type, spawn, { detonationDst: this.dst, }); if (this.mg.hasOwner(this.dst)) { diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts index 0e61470ad..1f7dd52eb 100644 --- a/src/core/execution/PortExecution.ts +++ b/src/core/execution/PortExecution.ts @@ -45,7 +45,7 @@ export class PortExecution implements Execution { this.active = false; return; } - this.port = player.buildUnit(UnitType.Port, 0, spawn); + this.port = player.buildUnit(UnitType.Port, spawn, {}); } if (!this.port.isActive()) { diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index c3ebbf54d..511d3c5fa 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -99,7 +99,7 @@ export class SAMLauncherExecution implements Execution { this.active = false; return; } - this.sam = this.player.buildUnit(UnitType.SAMLauncher, 0, spawnTile, { + this.sam = this.player.buildUnit(UnitType.SAMLauncher, spawnTile, { cooldownDuration: this.mg.config().SAMCooldown(), }); } diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts index 959c53330..32ce661b2 100644 --- a/src/core/execution/SAMMissileExecution.ts +++ b/src/core/execution/SAMMissileExecution.ts @@ -33,8 +33,8 @@ export class SAMMissileExecution implements Execution { if (this.SAMMissile == null) { this.SAMMissile = this._owner.buildUnit( UnitType.SAMMissile, - 0, this.spawn, + {}, ); } if (!this.SAMMissile.isActive()) { diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts index 0fc149e11..2acc90cb3 100644 --- a/src/core/execution/ShellExecution.ts +++ b/src/core/execution/ShellExecution.ts @@ -24,7 +24,7 @@ export class ShellExecution implements Execution { tick(ticks: number): void { if (this.shell == null) { - this.shell = this._owner.buildUnit(UnitType.Shell, 0, this.spawn); + this.shell = this._owner.buildUnit(UnitType.Shell, this.spawn, {}); } if (!this.shell.isActive()) { this.active = false; diff --git a/src/core/execution/TradeShipExecution.ts b/src/core/execution/TradeShipExecution.ts index 297a0c4cf..6157e2063 100644 --- a/src/core/execution/TradeShipExecution.ts +++ b/src/core/execution/TradeShipExecution.ts @@ -45,7 +45,7 @@ export class TradeShipExecution implements Execution { this.active = false; return; } - this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, 0, spawn, { + this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, spawn, { dstPort: this._dstPort, lastSetSafeFromPirates: ticks, }); diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts index 11c294b73..0194c895d 100644 --- a/src/core/execution/TransportShipExecution.ts +++ b/src/core/execution/TransportShipExecution.ts @@ -139,11 +139,9 @@ export class TransportShipExecution implements Execution { } } - this.boat = this.attacker.buildUnit( - UnitType.TransportShip, - this.troops, - this.src, - ); + this.boat = this.attacker.buildUnit(UnitType.TransportShip, this.src, { + troops: this.troops, + }); } tick(ticks: number) { diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index db467d55d..74c450dd2 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -119,7 +119,7 @@ export class WarshipExecution implements Execution { this.active = false; return; } - this.warship = this._owner.buildUnit(UnitType.Warship, 0, spawn); + this.warship = this._owner.buildUnit(UnitType.Warship, spawn, {}); return; } if (!this.warship.isActive()) { diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index b00a57405..8eec8c263 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -148,6 +148,51 @@ export enum UnitType { Construction = "Construction", } +export interface UnitParamsMap { + [UnitType.TransportShip]: { + troops?: number; + destination?: TileRef; + }; + + [UnitType.Warship]: {}; + + [UnitType.Shell]: {}; + + [UnitType.SAMMissile]: {}; + + [UnitType.Port]: {}; + + [UnitType.AtomBomb]: {}; + + [UnitType.HydrogenBomb]: {}; + + [UnitType.TradeShip]: { + dstPort: Unit; + lastSetSafeFromPirates?: number; + }; + + [UnitType.MissileSilo]: { + cooldownDuration?: number; + }; + + [UnitType.DefensePost]: {}; + + [UnitType.SAMLauncher]: {}; + + [UnitType.City]: {}; + + [UnitType.MIRV]: {}; + + [UnitType.MIRVWarhead]: {}; + + [UnitType.Construction]: {}; +} + +// Type helper to get params type for a specific unit type +export type UnitParams = UnitParamsMap[T]; + +export type AllUnitParams = UnitParamsMap[keyof UnitParamsMap]; + export const nukeTypes = [ UnitType.AtomBomb, UnitType.HydrogenBomb, @@ -276,15 +321,6 @@ export class PlayerInfo { } } -// Some units have info specific to them -export interface UnitSpecificInfos { - dstPort?: Unit; // Only for trade ships - lastSetSafeFromPirates?: number; // Only for trade ships - detonationDst?: TileRef; // Only for nukes - warshipTarget?: Unit; - cooldownDuration?: number; -} - export interface Unit { id(): number; @@ -391,12 +427,12 @@ export interface Player { unitsIncludingConstruction(type: UnitType): Unit[]; buildableUnits(tile: TileRef): BuildableUnit[]; canBuild(type: UnitType, targetTile: TileRef): TileRef | false; - buildUnit( - type: UnitType, - troops: number, - tile: TileRef, - unitSpecificInfos?: UnitSpecificInfos, + buildUnit( + type: T, + spawnTile: TileRef, + params: UnitParams, ): Unit; + captureUnit(unit: Unit): void; // Relations & Diplomacy diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 55972adc5..0a98ff41a 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -35,7 +35,7 @@ import { TerraNullius, Tick, Unit, - UnitSpecificInfos, + UnitParams, UnitType, } from "./Game"; import { GameImpl } from "./GameImpl"; @@ -703,11 +703,10 @@ export class PlayerImpl implements Player { ); } - buildUnit( - type: UnitType, - troops: number, + buildUnit( + type: T, spawnTile: TileRef, - unitSpecificInfos: UnitSpecificInfos = {}, + params: UnitParams, ): UnitImpl { if (this.mg.config().isUnitDisabled(type)) { throw new Error( @@ -720,14 +719,13 @@ export class PlayerImpl implements Player { type, this.mg, spawnTile, - troops, this.mg.nextUnitID(), this, - unitSpecificInfos, + params, ); this._units.push(b); this.removeGold(cost); - this.removeTroops(troops); + this.removeTroops("troops" in params ? params.troops : 0); this.mg.addUpdate(b.toUpdate()); this.mg.addUnit(b); diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index 247e0eced..9c1458f51 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -1,11 +1,11 @@ import { simpleHash, toInt, withinInt } from "../Util"; import { + AllUnitParams, MessageType, Player, Tick, Unit, UnitInfo, - UnitSpecificInfos, UnitType, } from "./Game"; import { GameImpl } from "./GameImpl"; @@ -24,6 +24,7 @@ export class UnitImpl implements Unit { private _lastSetSafeFromPirates: number; // Only for trade ships private _constructionType: UnitType = undefined; + private _troops: number; private _cooldownTick: Tick | null = null; private _dstPort: Unit | null = null; // Only for trade ships private _detonationDst: TileRef | null = null; // Only for nukes @@ -34,21 +35,22 @@ export class UnitImpl implements Unit { private _type: UnitType, private mg: GameImpl, private _tile: TileRef, - private _troops: number, private _id: number, public _owner: PlayerImpl, - unitsSpecificInfos: UnitSpecificInfos = {}, + params: AllUnitParams = {}, ) { - this._health = toInt(this.mg.unitInfo(_type).maxHealth ?? 1); this._lastTile = _tile; - this._dstPort = unitsSpecificInfos.dstPort; - this._detonationDst = unitsSpecificInfos.detonationDst; - this._warshipTarget = unitsSpecificInfos.warshipTarget; - this._cooldownDuration = unitsSpecificInfos.cooldownDuration; - this._lastSetSafeFromPirates = unitsSpecificInfos.lastSetSafeFromPirates; + this._health = toInt(this.mg.unitInfo(_type).maxHealth ?? 1); this._safeFromPiratesCooldown = this.mg .config() .safeFromPiratesCooldownMax(); + + this._troops = "troops" in params ? params.troops : 0; + this._dstPort = "dstPort" in params ? params.dstPort : null; + this._cooldownDuration = + "cooldownDuration" in params ? params.cooldownDuration : null; + this._lastSetSafeFromPirates = + "lastSetSafeFromPirates" in params ? params.lastSetSafeFromPirates : 0; } id() { diff --git a/tests/SAM.test.ts b/tests/SAM.test.ts index b3600310b..51d267858 100644 --- a/tests/SAM.test.ts +++ b/tests/SAM.test.ts @@ -50,9 +50,9 @@ describe("SAM", () => { }); test("one sam should take down one nuke", async () => { - const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1)); + const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {}); game.addExecution(new SAMLauncherExecution(defender.id(), null, sam)); - attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 1)); + attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), {}); executeTicks(game, 3); @@ -60,10 +60,14 @@ describe("SAM", () => { }); test("sam should only get one nuke at a time", async () => { - const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1)); + const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {}); game.addExecution(new SAMLauncherExecution(defender.id(), null, sam)); - attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(2, 1)); - attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 2)); + attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 1), { + detonationDst: game.ref(2, 1), + }); + attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), { + detonationDst: game.ref(1, 2), + }); expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2); executeTicks(game, 3); @@ -72,10 +76,12 @@ describe("SAM", () => { }); test("sam should cooldown as long as configured", async () => { - const sam = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1)); + const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {}); game.addExecution(new SAMLauncherExecution(defender.id(), null, sam)); expect(sam.isCooldown()).toBeFalsy(); - const nuke = attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(1, 2)); + const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), { + detonationDst: game.ref(1, 2), + }); executeTicks(game, 3); @@ -91,11 +97,15 @@ describe("SAM", () => { }); test("two sams should not target twice same nuke", async () => { - const sam1 = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 1)); + const sam1 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), { + cooldownDuration: 10, + }); game.addExecution(new SAMLauncherExecution(defender.id(), null, sam1)); - const sam2 = defender.buildUnit(UnitType.SAMLauncher, 0, game.ref(1, 2)); + const sam2 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 2), {}); game.addExecution(new SAMLauncherExecution(defender.id(), null, sam2)); - const nuke = attacker.buildUnit(UnitType.AtomBomb, 0, game.ref(2, 2)); + const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 2), { + detonationDst: game.ref(2, 2), + }); executeTicks(game, 3); diff --git a/tests/Warship.test.ts b/tests/Warship.test.ts index c1ca61ecf..dc1905739 100644 --- a/tests/Warship.test.ts +++ b/tests/Warship.test.ts @@ -59,11 +59,11 @@ describe("Warship", () => { test("Warship heals only if player has port", async () => { const maxHealth = game.config().unitInfo(UnitType.Warship).maxHealth; - const port = player1.buildUnit(UnitType.Port, 0, game.ref(coastX, 10)); + const port = player1.buildUnit(UnitType.Port, game.ref(coastX, 10), {}); const warship = player1.buildUnit( UnitType.Warship, - 0, game.ref(coastX + 1, 10), + {}, ); game.executeNextTick(); @@ -91,8 +91,10 @@ describe("Warship", () => { // we can obviously directly add it to the player) const tradeShip = player2.buildUnit( UnitType.TradeShip, - 0, game.ref(coastX + 1, 7), + { + dstPort: null, + }, ); expect(tradeShip.owner().id()).toBe(player2.id()); @@ -113,8 +115,10 @@ describe("Warship", () => { // we can obviously directly add it to the player) const tradeShip = player2.buildUnit( UnitType.TradeShip, - 0, game.ref(coastX + 1, 11), + { + dstPort: null, + }, ); expect(tradeShip.owner().id()).toBe(player2.id()); From d5ac65dea6451cbd381f0fed7208836a64a20d16 Mon Sep 17 00:00:00 2001 From: Rouillard Date: Sun, 11 May 2025 22:06:07 +0200 Subject: [PATCH 2/3] Fix the clipping bug when a warship has no path (#725) ## Description: Fix the clipping issue when a warship does not move. Add a move order to current position when warship has completed its current path or is pending a new path. ![image](https://github.com/user-attachments/assets/e529fb9c-8969-490a-aada-ce0d2b3d5b84) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: --- src/core/execution/WarshipExecution.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index 74c450dd2..d96373b1e 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -54,11 +54,13 @@ export class WarshipExecution implements Execution { switch (result.type) { case PathFindResultType.Completed: this.warship.setMoveTarget(null); + this.warship.move(this.warship.tile()); return; case PathFindResultType.NextTile: this.warship.move(result.tile); break; case PathFindResultType.Pending: + this.warship.move(this.warship.tile()); break; case PathFindResultType.PathNotFound: consolex.log(`path not found to target`); @@ -98,11 +100,13 @@ export class WarshipExecution implements Execution { switch (result.type) { case PathFindResultType.Completed: this.patrolTile = this.randomTile(); + this.warship.move(this.warship.tile()); break; case PathFindResultType.NextTile: this.warship.move(result.tile); break; case PathFindResultType.Pending: + this.warship.move(this.warship.tile()); return; case PathFindResultType.PathNotFound: consolex.log(`path not found to patrol tile`); @@ -227,11 +231,13 @@ export class WarshipExecution implements Execution { case PathFindResultType.Completed: this._owner.captureUnit(this.target); this.target = null; + this.warship.move(this.warship.tile()); return; case PathFindResultType.NextTile: this.warship.move(result.tile); break; case PathFindResultType.Pending: + this.warship.move(this.warship.tile()); break; case PathFindResultType.PathNotFound: consolex.log(`path not found to target`); From 0dc68ced31b5031c8d405479b9dbeeb3334b482e Mon Sep 17 00:00:00 2001 From: evanpelle Date: Sun, 11 May 2025 13:28:38 -0700 Subject: [PATCH 3/3] Add pause button when replaying (#726) ## Description: This PR does two things: 1. Allow pausing on replay 2. As part of the refactoring, in singleplayer games, LocalServer now waits for the last turn to complete execution before sending the next turn. Previously, low end devices would sometimes fall behind getting the "playing the past" bug where commands were delayed. Now when a devices cannot keep up, the entire game slows down. ## Please complete the following: - [ ] I have added screenshots for all UI updates - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [ ] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: Co-authored-by: evan --- src/client/ClientGameRunner.ts | 2 + src/client/LocalServer.ts | 50 +++++++++++++++++------ src/client/Transport.ts | 13 +++++- src/client/graphics/layers/OptionsMenu.ts | 3 +- src/core/configuration/Config.ts | 1 + src/core/configuration/ConfigLoader.ts | 5 ++- src/core/configuration/DefaultConfig.ts | 4 ++ src/core/configuration/DevConfig.ts | 9 +++- tests/util/Setup.ts | 7 +++- 9 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index f782ee0b9..d0c4de33c 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -112,6 +112,7 @@ export async function createClientGame( const config = await getConfig( lobbyConfig.gameStartInfo.config, userSettings, + lobbyConfig.gameRecord != null, ); let gameMap: TerrainMapData | null = null; @@ -243,6 +244,7 @@ export class ClientGameRunner { this.stop(true); return; } + this.transport.turnComplete(); gu.updates[GameUpdateType.Hash].forEach((hu: HashUpdate) => { this.eventBus.emit(new SendHashEvent(hu.tick, hu.hash)); }); diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index 42a777129..852d6fb64 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -16,42 +16,58 @@ import { LobbyConfig } from "./ClientGameRunner"; import { getPersistentIDFromCookie } from "./Main"; export class LocalServer { + // All turns from the game record on replay. + private replayTurns: Turn[] = []; + private turns: Turn[] = []; + private intents: Intent[] = []; private startedAt: number; - private endTurnIntervalID; - private paused = false; private winner: ClientSendWinnerMessage = null; private allPlayersStats: AllPlayersStats = {}; + private turnsExecuted = 0; + private lastTurnCompletedTime = 0; + + private turnCheckInterval: NodeJS.Timeout; + constructor( private lobbyConfig: LobbyConfig, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void, + private isReplay: boolean, ) {} start() { + this.turnCheckInterval = setInterval(() => { + if (this.turnsExecuted == this.turns.length) { + if ( + this.isReplay || + Date.now() > + this.lastTurnCompletedTime + + this.lobbyConfig.serverConfig.turnIntervalMs() + ) { + this.endTurn(); + } + } + }, 5); + this.startedAt = Date.now(); - if (!this.lobbyConfig.gameRecord) { - this.endTurnIntervalID = setInterval( - () => this.endTurn(), - this.lobbyConfig.serverConfig.turnIntervalMs(), - ); - } this.clientConnect(); if (this.lobbyConfig.gameRecord) { - this.turns = decompressGameRecord(this.lobbyConfig.gameRecord).turns; - console.log(`loaded turns: ${JSON.stringify(this.turns)}`); + this.replayTurns = decompressGameRecord( + this.lobbyConfig.gameRecord, + ).turns; } this.clientMessage( ServerStartGameMessageSchema.parse({ type: "start", gameID: this.lobbyConfig.gameStartInfo.gameID, gameStartInfo: this.lobbyConfig.gameStartInfo, - turns: this.turns, + turns: [], }), ); } @@ -90,7 +106,7 @@ export class LocalServer { return; } // If we are replaying a game then verify hash. - const archivedHash = this.turns[clientMsg.turnNumber].hash; + const archivedHash = this.replayTurns[clientMsg.turnNumber].hash; if (!archivedHash) { console.warn( `no archived hash found for turn ${clientMsg.turnNumber}, client hash: ${clientMsg.hash}`, @@ -121,10 +137,18 @@ export class LocalServer { } } + public turnComplete() { + this.turnsExecuted++; + this.lastTurnCompletedTime = Date.now(); + } + private endTurn() { if (this.paused) { return; } + if (this.replayTurns.length > 0) { + this.intents = this.replayTurns[this.turns.length].intents; + } const pastTurn: Turn = { turnNumber: this.turns.length, intents: this.intents, @@ -139,7 +163,7 @@ export class LocalServer { public endGame(saveFullGame: boolean = false) { consolex.log("local server ending game"); - clearInterval(this.endTurnIntervalID); + clearInterval(this.turnCheckInterval); const players: PlayerRecord[] = [ { ip: null, diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 73c9fee97..6c52e4e17 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -263,7 +263,12 @@ export class Transport { onconnect: () => void, onmessage: (message: ServerMessage) => void, ) { - this.localServer = new LocalServer(this.lobbyConfig, onconnect, onmessage); + this.localServer = new LocalServer( + this.lobbyConfig, + onconnect, + onmessage, + this.lobbyConfig.gameRecord != null, + ); this.localServer.start(); } @@ -318,6 +323,12 @@ export class Transport { this.connect(this.onconnect, this.onmessage); } + public turnComplete() { + if (this.isLocal) { + this.localServer.turnComplete(); + } + } + private onSendLogEvent(event: SendLogEvent) { this.sendMsg( JSON.stringify({ diff --git a/src/client/graphics/layers/OptionsMenu.ts b/src/client/graphics/layers/OptionsMenu.ts index cad75e929..5d69da364 100644 --- a/src/client/graphics/layers/OptionsMenu.ts +++ b/src/client/graphics/layers/OptionsMenu.ts @@ -122,7 +122,8 @@ export class OptionsMenu extends LitElement implements Layer { init() { console.log("init called from OptionsMenu"); this.showPauseButton = - this.game.config().gameConfig().gameType == GameType.Singleplayer; + this.game.config().gameConfig().gameType == GameType.Singleplayer || + this.game.config().isReplay(); this.isVisible = true; this.requestUpdate(); } diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 93e2f54c9..26564eaf5 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -136,6 +136,7 @@ export interface Config { defaultNukeSpeed(): number; nukeDeathFactor(humans: number, tilesOwned: number): number; structureMinDist(): number; + isReplay(): boolean; } export interface Theme { diff --git a/src/core/configuration/ConfigLoader.ts b/src/core/configuration/ConfigLoader.ts index 3954e6a4c..342b425fc 100644 --- a/src/core/configuration/ConfigLoader.ts +++ b/src/core/configuration/ConfigLoader.ts @@ -12,15 +12,16 @@ export let cachedSC: ServerConfig = null; export async function getConfig( gameConfig: GameConfig, userSettings: UserSettings | null = null, + isReplay: boolean = false, ): Promise { const sc = await getServerConfigFromClient(); switch (sc.env()) { case GameEnv.Dev: - return new DevConfig(sc, gameConfig, userSettings); + return new DevConfig(sc, gameConfig, userSettings, isReplay); case GameEnv.Preprod: case GameEnv.Prod: consolex.log("using prod config"); - return new DefaultConfig(sc, gameConfig, userSettings); + return new DefaultConfig(sc, gameConfig, userSettings, isReplay); default: throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`); } diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index c9a4d09e0..12c1b63d8 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -158,7 +158,11 @@ export class DefaultConfig implements Config { private _serverConfig: ServerConfig, private _gameConfig: GameConfig, private _userSettings: UserSettings, + private _isReplay: boolean, ) {} + isReplay(): boolean { + return this._isReplay; + } samHittingChance(): number { return 0.8; diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index 909e3a156..0f6f0f019 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -41,8 +41,13 @@ export class DevServerConfig extends DefaultServerConfig { } export class DevConfig extends DefaultConfig { - constructor(sc: ServerConfig, gc: GameConfig, us: UserSettings) { - super(sc, gc, us); + constructor( + sc: ServerConfig, + gc: GameConfig, + us: UserSettings, + isReplay: boolean, + ) { + super(sc, gc, us, isReplay); } // numSpawnPhaseTurns(): number { diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts index b77f74112..8168870e4 100644 --- a/tests/util/Setup.ts +++ b/tests/util/Setup.ts @@ -42,7 +42,12 @@ export async function setup( instantBuild: false, ..._gameConfig, }; - const config = new TestConfig(serverConfig, gameConfig, new UserSettings()); + const config = new TestConfig( + serverConfig, + gameConfig, + new UserSettings(), + false, + ); // Create and return the game return createGame(humans, [], gameMap, miniGameMap, config);