2025-12-31 18:49:26 +00:00
|
|
|
import { expect } from "@esm-bundle/chai";
|
|
|
|
|
import sinon from "sinon";
|
|
|
|
|
import {
|
|
|
|
|
gameStateManager,
|
|
|
|
|
GameStateManager,
|
|
|
|
|
} from "../../../src/core/GameStateManager.js";
|
|
|
|
|
|
|
|
|
|
describe("Core: GameStateManager - Hub Integration", () => {
|
|
|
|
|
let mockPersistence;
|
|
|
|
|
let mockGameLoop;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
gameStateManager.reset();
|
|
|
|
|
|
|
|
|
|
mockPersistence = {
|
|
|
|
|
init: sinon.stub().resolves(),
|
|
|
|
|
saveRun: sinon.stub().resolves(),
|
|
|
|
|
loadRun: sinon.stub().resolves(null),
|
|
|
|
|
loadRoster: sinon.stub().resolves(null),
|
|
|
|
|
saveRoster: sinon.stub().resolves(),
|
2025-12-31 23:06:07 +00:00
|
|
|
loadCampaign: sinon.stub().resolves(null),
|
|
|
|
|
saveCampaign: sinon.stub().resolves(),
|
|
|
|
|
loadMarketState: sinon.stub().resolves(null),
|
|
|
|
|
saveMarketState: sinon.stub().resolves(),
|
2025-12-31 18:49:26 +00:00
|
|
|
};
|
|
|
|
|
gameStateManager.persistence = mockPersistence;
|
|
|
|
|
|
|
|
|
|
mockGameLoop = {
|
|
|
|
|
init: sinon.spy(),
|
|
|
|
|
startLevel: sinon.stub().resolves(),
|
|
|
|
|
stop: sinon.spy(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
gameStateManager.missionManager = {
|
|
|
|
|
setupActiveMission: sinon.stub(),
|
|
|
|
|
getActiveMission: sinon.stub().returns({
|
|
|
|
|
id: "MISSION_TUTORIAL_01",
|
|
|
|
|
config: { title: "Test Mission" },
|
|
|
|
|
biome: {
|
|
|
|
|
generator_config: {
|
|
|
|
|
seed_type: "RANDOM",
|
|
|
|
|
seed: 12345,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
objectives: [],
|
|
|
|
|
}),
|
|
|
|
|
playIntro: sinon.stub().resolves(),
|
|
|
|
|
missionRegistry: new Map(),
|
2025-12-31 23:06:07 +00:00
|
|
|
load: sinon.stub(),
|
|
|
|
|
save: sinon.stub().returns({ completedMissions: [] }),
|
|
|
|
|
completedMissions: new Set(),
|
2025-12-31 18:49:26 +00:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("continueGame - Hub vs Main Menu Logic", () => {
|
|
|
|
|
it("should go to Hub when there's campaign progress but no active run", async () => {
|
|
|
|
|
// Setup: roster exists (campaign progress)
|
|
|
|
|
gameStateManager.rosterManager.roster = [
|
|
|
|
|
{ id: "u1", name: "Test Unit", status: "READY" },
|
|
|
|
|
];
|
|
|
|
|
mockPersistence.loadRun.resolves(null); // No active run
|
|
|
|
|
|
|
|
|
|
await gameStateManager.init();
|
|
|
|
|
const transitionSpy = sinon.spy(gameStateManager, "transitionTo");
|
|
|
|
|
|
|
|
|
|
await gameStateManager.continueGame();
|
|
|
|
|
|
|
|
|
|
expect(mockPersistence.loadRun.called).to.be.true;
|
|
|
|
|
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be.true;
|
|
|
|
|
// Hub should show because roster exists
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should resume active run when save exists", async () => {
|
|
|
|
|
const savedRun = {
|
|
|
|
|
id: "RUN_123",
|
|
|
|
|
missionId: "MISSION_TUTORIAL_01",
|
|
|
|
|
seed: 12345,
|
|
|
|
|
depth: 1,
|
|
|
|
|
squad: [],
|
|
|
|
|
};
|
|
|
|
|
mockPersistence.loadRun.resolves(savedRun);
|
|
|
|
|
gameStateManager.setGameLoop(mockGameLoop);
|
|
|
|
|
|
|
|
|
|
await gameStateManager.init();
|
|
|
|
|
await gameStateManager.continueGame();
|
|
|
|
|
|
|
|
|
|
expect(mockPersistence.loadRun.called).to.be.true;
|
|
|
|
|
expect(gameStateManager.activeRunData).to.deep.equal(savedRun);
|
|
|
|
|
expect(mockGameLoop.startLevel.called).to.be.true;
|
|
|
|
|
// Should transition to DEPLOYMENT, not MAIN_MENU
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should go to Hub when completed missions exist but no active run", async () => {
|
|
|
|
|
gameStateManager.missionManager.completedMissions.add("MISSION_TUTORIAL_01");
|
|
|
|
|
mockPersistence.loadRun.resolves(null);
|
|
|
|
|
|
|
|
|
|
await gameStateManager.init();
|
|
|
|
|
const transitionSpy = sinon.spy(gameStateManager, "transitionTo");
|
|
|
|
|
|
|
|
|
|
await gameStateManager.continueGame();
|
|
|
|
|
|
|
|
|
|
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be.true;
|
|
|
|
|
// Hub should show because completed missions exist
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should stay on main menu when no campaign progress and no active run", async () => {
|
|
|
|
|
// No roster, no completed missions, no active run
|
|
|
|
|
gameStateManager.rosterManager.roster = [];
|
|
|
|
|
gameStateManager.missionManager.completedMissions.clear();
|
|
|
|
|
mockPersistence.loadRun.resolves(null);
|
|
|
|
|
|
|
|
|
|
await gameStateManager.init();
|
|
|
|
|
const transitionSpy = sinon.spy(gameStateManager, "transitionTo");
|
|
|
|
|
|
|
|
|
|
await gameStateManager.continueGame();
|
|
|
|
|
|
|
|
|
|
// Should not transition (stays on current state)
|
|
|
|
|
// Main menu should remain visible
|
|
|
|
|
expect(transitionSpy.called).to.be.false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should prioritize active run over campaign progress", async () => {
|
|
|
|
|
// Both active run and campaign progress exist
|
|
|
|
|
const savedRun = {
|
|
|
|
|
id: "RUN_123",
|
|
|
|
|
missionId: "MISSION_TUTORIAL_01",
|
|
|
|
|
seed: 12345,
|
|
|
|
|
depth: 1,
|
|
|
|
|
squad: [],
|
|
|
|
|
};
|
|
|
|
|
mockPersistence.loadRun.resolves(savedRun);
|
|
|
|
|
gameStateManager.rosterManager.roster = [
|
|
|
|
|
{ id: "u1", name: "Test Unit", status: "READY" },
|
|
|
|
|
];
|
|
|
|
|
gameStateManager.setGameLoop(mockGameLoop);
|
|
|
|
|
|
|
|
|
|
await gameStateManager.init();
|
|
|
|
|
await gameStateManager.continueGame();
|
|
|
|
|
|
|
|
|
|
// Should resume run, not go to hub
|
|
|
|
|
expect(mockGameLoop.startLevel.called).to.be.true;
|
|
|
|
|
expect(gameStateManager.activeRunData).to.deep.equal(savedRun);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("State Transitions - Hub Visibility", () => {
|
|
|
|
|
it("should transition to MAIN_MENU after mission completion", async () => {
|
|
|
|
|
// This simulates what happens after mission victory
|
|
|
|
|
await gameStateManager.init();
|
|
|
|
|
gameStateManager.rosterManager.roster = [
|
|
|
|
|
{ id: "u1", name: "Test Unit", status: "READY" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const transitionSpy = sinon.spy(gameStateManager, "transitionTo");
|
|
|
|
|
await gameStateManager.transitionTo(GameStateManager.STATES.MAIN_MENU);
|
|
|
|
|
|
|
|
|
|
expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be.true;
|
|
|
|
|
// Hub should be shown because roster exists
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|