diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 702abf110..914152163 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -100,6 +100,9 @@ export class GameRunner { if (this.game.config().gameConfig().gameType !== GameType.Singleplayer) { this.game.addExecution(new SpawnTimerExecution()); } + if (this.game.config().spawnNations()) { + this.game.addExecution(...this.execManager.nationExecutions()); + } if (this.game.config().isRandomSpawn()) { this.game.addExecution(...this.execManager.spawnPlayers()); } @@ -108,9 +111,6 @@ export class GameRunner { ...this.execManager.spawnTribes(this.game.config().bots()), ); } - if (this.game.config().spawnNations()) { - this.game.addExecution(...this.execManager.nationExecutions()); - } this.game.addExecution(new WinCheckExecution()); if (!this.game.config().isUnitDisabled(UnitType.Factory)) { this.game.addExecution( diff --git a/src/core/execution/NationExecution.ts b/src/core/execution/NationExecution.ts index 6a2a53a7d..6cc60cdb3 100644 --- a/src/core/execution/NationExecution.ts +++ b/src/core/execution/NationExecution.ts @@ -161,6 +161,11 @@ export class NationExecution implements Execution { return; } + // Spawn phase already ended but our SpawnExecution hasn't fired yet — wait. + if (this.spawnExecAdded && !this.player.hasSpawned()) { + return; + } + if (!this.player.isAlive()) { //removeOnDeath is called from nation's PlayerExecution this.active = false; diff --git a/tests/core/GameRunner.test.ts b/tests/core/GameRunner.test.ts new file mode 100644 index 000000000..f02b07823 --- /dev/null +++ b/tests/core/GameRunner.test.ts @@ -0,0 +1,105 @@ +import { NationExecution } from "../../src/core/execution/NationExecution"; +import { SpawnExecution } from "../../src/core/execution/SpawnExecution"; +import { Cell, Nation, PlayerInfo, PlayerType } from "../../src/core/game/Game"; +import { GameConfig, GameID } from "../../src/core/Schemas"; +import { setup } from "../util/Setup"; +import { executeTicks } from "../util/utils"; + +const gameID: GameID = "test_game_id"; + +async function createTestGame( + randomSpawn: boolean, + nationCells: { x: number; y: number }[], +) { + const game = await setup( + "plains", + { randomSpawn } as Partial, + [], + undefined, + undefined, + false, + ); + + const humanInfo = new PlayerInfo( + "human", + PlayerType.Human, + "client_1", + "human_id", + ); + game.addPlayer(humanInfo); + + const nations: { info: PlayerInfo; nation: Nation }[] = []; + for (let i = 0; i < nationCells.length; i++) { + const info = new PlayerInfo( + nationCells.length === 1 ? "TestNation" : `Nation${i}`, + PlayerType.Nation, + null, + nationCells.length === 1 ? "nation_id" : `nation_${i}`, + ); + const nation = new Nation( + new Cell(nationCells[i].x, nationCells[i].y), + info, + ); + game.addPlayer(info); + nations.push({ info, nation }); + } + + return { game, humanInfo, nations }; +} + +describe("Nation spawn ordering with random spawn", () => { + test("nation spawns in singleplayer with random spawn", async () => { + const { game, humanInfo, nations } = await createTestGame(true, [ + { x: 50, y: 50 }, + ]); + + // Mirror GameRunner.init() ordering: nation first, then human. + game.addExecution(new NationExecution(gameID, nations[0].nation)); + game.addExecution( + new SpawnExecution(gameID, game.player(humanInfo.id).info()), + ); + + executeTicks(game, 4); + + expect(game.player(humanInfo.id).hasSpawned()).toBe(true); + expect(game.player(nations[0].info.id).hasSpawned()).toBe(true); + expect(game.player(nations[0].info.id).isAlive()).toBe(true); + }); + + test("multiple nations spawn in singleplayer with random spawn", async () => { + const cells = Array.from({ length: 5 }, (_, i) => ({ + x: 20 + i * 15, + y: 20 + i * 15, + })); + const { game, humanInfo, nations } = await createTestGame(true, cells); + + // Nation executions first (mirrors GameRunner.init()). + for (const { nation } of nations) { + game.addExecution(new NationExecution(gameID, nation)); + } + // Human spawn execution second. + game.addExecution( + new SpawnExecution(gameID, game.player(humanInfo.id).info()), + ); + + executeTicks(game, 8); + + expect(game.player(humanInfo.id).hasSpawned()).toBe(true); + for (const { info } of nations) { + const player = game.player(info.id); + expect(player.hasSpawned()).toBe(true); + expect(player.isAlive()).toBe(true); + } + }); + + test("nation spawns in singleplayer without random spawn", async () => { + const { game, nations } = await createTestGame(false, [{ x: 50, y: 50 }]); + + game.addExecution(new NationExecution(gameID, nations[0].nation)); + + executeTicks(game, 4); + + expect(game.player(nations[0].info.id).hasSpawned()).toBe(true); + expect(game.player(nations[0].info.id).isAlive()).toBe(true); + }); +});