refactor: Update GameLoop and StateManager logic

This commit is contained in:
Matthew Mone 2026-01-14 11:12:04 -08:00
parent dbfa9929dd
commit 234ce4b5f3
3 changed files with 75 additions and 15 deletions

View file

@ -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";

View file

@ -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.
});
}

View file

@ -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;
});
});