refactor: Update GameLoop and StateManager logic
This commit is contained in:
parent
dbfa9929dd
commit
234ce4b5f3
3 changed files with 75 additions and 15 deletions
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue