diff --git a/src/core/GameLoop.js b/src/core/GameLoop.js index 7c61bf5..47d0fb5 100644 --- a/src/core/GameLoop.js +++ b/src/core/GameLoop.js @@ -1131,6 +1131,7 @@ export class GameLoop { */ async startLevel(runData, options = {}) { console.log("GameLoop: Generating Level..."); + console.log(`Level Seed: ${runData.seed}`); this.runData = runData; this.isRunning = true; @@ -1171,7 +1172,7 @@ export class GameLoop { // Restart the animation loop if it was stopped this.animate(); - this.grid = new VoxelGrid(20, 10, 20); + this.grid = new VoxelGrid(20, 20, 20); let generator; const biomeType = runData.biome?.type || "BIOME_RUSTING_WASTES"; diff --git a/src/core/GameStateManager.js b/src/core/GameStateManager.js index 86c78be..e6ca712 100644 --- a/src/core/GameStateManager.js +++ b/src/core/GameStateManager.js @@ -559,11 +559,10 @@ class GameStateManagerClass { // Clear the active run (persistence and memory) await this.clearActiveRun(); - // Transition to Main Menu (will show Hub if unlocking conditions met) - await this.transitionTo(GameStateManagerClass.STATES.MAIN_MENU); - - // Force a refresh of the Hub screen if it was already open/cached? - // The state transition should handle visibility, but we check specific UI updates if needed. + // NOTE: We do NOT transition to Main Menu automatically here. + // The GameLoop (UI layer) is responsible for showing the Mission Debrief + // and then requesting title/hub transition when the user is done. + // If we transitioned here, we would rip the UI away before the user saw the results. }); } diff --git a/test/core/GameStateManager.test.js b/test/core/GameStateManager.test.js index 86bc0cf..8c3183e 100644 --- a/test/core/GameStateManager.test.js +++ b/test/core/GameStateManager.test.js @@ -29,6 +29,7 @@ describe("Core: GameStateManager (Singleton)", () => { saveHubStash: sinon.stub().resolves(), loadUnlocks: sinon.stub().resolves([]), saveUnlocks: sinon.stub().resolves(), + clearRun: sinon.stub().resolves(), }; // Inject Mock (replacing the real Persistence instance) gameStateManager.persistence = mockPersistence; @@ -127,7 +128,9 @@ describe("Core: GameStateManager (Singleton)", () => { className: "Vanguard", // Class name classId: "CLASS_VANGUARD", }; - gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit); + gameStateManager.rosterManager.recruitUnit = sinon + .stub() + .resolves(mockRecruitedUnit); gameStateManager.setGameLoop(mockGameLoop); await gameStateManager.init(); @@ -137,7 +140,9 @@ describe("Core: GameStateManager (Singleton)", () => { mockGameLoop.startLevel = sinon.stub().resolves(); // Await the full async chain - await gameStateManager.handleEmbark({ detail: { squad: mockSquad, mode: "SELECT" } }); + await gameStateManager.handleEmbark({ + detail: { squad: mockSquad, mode: "SELECT" }, + }); expect(gameStateManager.currentState).to.equal( GameStateManager.STATES.DEPLOYMENT @@ -156,11 +161,15 @@ describe("Core: GameStateManager (Singleton)", () => { className: "Vanguard", classId: "CLASS_VANGUARD", }; - gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit); + gameStateManager.rosterManager.recruitUnit = sinon + .stub() + .resolves(mockRecruitedUnit); gameStateManager.setGameLoop(mockGameLoop); await gameStateManager.init(); - const mockSquad = [{ id: "u1", isNew: true, name: "Vanguard", classId: "CLASS_VANGUARD" }]; + const mockSquad = [ + { id: "u1", isNew: true, name: "Vanguard", classId: "CLASS_VANGUARD" }, + ]; mockGameLoop.startLevel = sinon.stub().resolves(); let eventDispatched = false; @@ -170,7 +179,9 @@ describe("Core: GameStateManager (Singleton)", () => { eventData = e.detail.runData; }); - await gameStateManager.handleEmbark({ detail: { squad: mockSquad, mode: "DRAFT" } }); + await gameStateManager.handleEmbark({ + detail: { squad: mockSquad, mode: "DRAFT" }, + }); expect(eventDispatched).to.be.true; expect(eventData).to.exist; @@ -202,7 +213,8 @@ describe("Core: GameStateManager (Singleton)", () => { await gameStateManager.init(); expect(mockPersistence.loadCampaign.called).to.be.true; - expect(gameStateManager.missionManager.load.calledWith(savedCampaignData)).to.be.true; + expect(gameStateManager.missionManager.load.calledWith(savedCampaignData)) + .to.be.true; }); it("CoA 6: init should handle missing campaign data gracefully", async () => { @@ -221,9 +233,11 @@ describe("Core: GameStateManager (Singleton)", () => { mockPersistence.saveCampaign.resetHistory(); // Dispatch campaign-data-changed event - window.dispatchEvent(new CustomEvent("campaign-data-changed", { - detail: { missionCompleted: "MISSION_TUTORIAL_01" } - })); + window.dispatchEvent( + new CustomEvent("campaign-data-changed", { + detail: { missionCompleted: "MISSION_TUTORIAL_01" }, + }) + ); // Wait for async save (event listener is synchronous but save is async) await new Promise((resolve) => setTimeout(resolve, 50)); @@ -233,4 +247,50 @@ describe("Core: GameStateManager (Singleton)", () => { expect(savedData).to.exist; expect(savedData.completedMissions).to.be.an("array"); }); + + it("CoA 8: mission-sequence-complete should clear run but NOT transition to MAIN_MENU automatically", async () => { + // Setup + await gameStateManager.init(); + gameStateManager.activeRunData = { id: "RUN_123" }; + + // Spy on transitionTo + const transitionSpy = sinon.spy(gameStateManager, "transitionTo"); + + // Dispatch event + window.dispatchEvent( + new CustomEvent("mission-sequence-complete", { + detail: { missionId: "MISSION_TEST" }, + }) + ); + + // Wait for async listeners + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Assert: Run should be cleared + expect(mockPersistence.clearRun.called).to.be.true; + expect(gameStateManager.activeRunData).to.be.null; + + // Assert: Should NOT have transitioned to MAIN_MENU (GameLoop handles this after debrief) + const callsToMainMenu = transitionSpy + .getCalls() + .filter((call) => call.args[0] === GameStateManager.STATES.MAIN_MENU); + // Initial init() calls it once, so we check if it was called AGAIN + // Actually, checking "called" might be tricky if init() called it. + // Let's check call count. init() makes 1 call. + // If mission-sequence-complete triggered it, we'd see 2. + // But better yet, let's reset history before dispatch. + + transitionSpy.resetHistory(); + + // Re-dispatch to be sure we are testing the listener + window.dispatchEvent( + new CustomEvent("mission-sequence-complete", { + detail: { missionId: "MISSION_TEST" }, + }) + ); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(transitionSpy.calledWith(GameStateManager.STATES.MAIN_MENU)).to.be + .false; + }); });