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 = {}) {
|
async startLevel(runData, options = {}) {
|
||||||
console.log("GameLoop: Generating Level...");
|
console.log("GameLoop: Generating Level...");
|
||||||
|
console.log(`Level Seed: ${runData.seed}`);
|
||||||
this.runData = runData;
|
this.runData = runData;
|
||||||
this.isRunning = true;
|
this.isRunning = true;
|
||||||
|
|
||||||
|
|
@ -1171,7 +1172,7 @@ export class GameLoop {
|
||||||
// Restart the animation loop if it was stopped
|
// Restart the animation loop if it was stopped
|
||||||
this.animate();
|
this.animate();
|
||||||
|
|
||||||
this.grid = new VoxelGrid(20, 10, 20);
|
this.grid = new VoxelGrid(20, 20, 20);
|
||||||
|
|
||||||
let generator;
|
let generator;
|
||||||
const biomeType = runData.biome?.type || "BIOME_RUSTING_WASTES";
|
const biomeType = runData.biome?.type || "BIOME_RUSTING_WASTES";
|
||||||
|
|
|
||||||
|
|
@ -559,11 +559,10 @@ class GameStateManagerClass {
|
||||||
// Clear the active run (persistence and memory)
|
// Clear the active run (persistence and memory)
|
||||||
await this.clearActiveRun();
|
await this.clearActiveRun();
|
||||||
|
|
||||||
// Transition to Main Menu (will show Hub if unlocking conditions met)
|
// NOTE: We do NOT transition to Main Menu automatically here.
|
||||||
await this.transitionTo(GameStateManagerClass.STATES.MAIN_MENU);
|
// The GameLoop (UI layer) is responsible for showing the Mission Debrief
|
||||||
|
// and then requesting title/hub transition when the user is done.
|
||||||
// Force a refresh of the Hub screen if it was already open/cached?
|
// If we transitioned here, we would rip the UI away before the user saw the results.
|
||||||
// The state transition should handle visibility, but we check specific UI updates if needed.
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
saveHubStash: sinon.stub().resolves(),
|
saveHubStash: sinon.stub().resolves(),
|
||||||
loadUnlocks: sinon.stub().resolves([]),
|
loadUnlocks: sinon.stub().resolves([]),
|
||||||
saveUnlocks: sinon.stub().resolves(),
|
saveUnlocks: sinon.stub().resolves(),
|
||||||
|
clearRun: sinon.stub().resolves(),
|
||||||
};
|
};
|
||||||
// Inject Mock (replacing the real Persistence instance)
|
// Inject Mock (replacing the real Persistence instance)
|
||||||
gameStateManager.persistence = mockPersistence;
|
gameStateManager.persistence = mockPersistence;
|
||||||
|
|
@ -127,7 +128,9 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
className: "Vanguard", // Class name
|
className: "Vanguard", // Class name
|
||||||
classId: "CLASS_VANGUARD",
|
classId: "CLASS_VANGUARD",
|
||||||
};
|
};
|
||||||
gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit);
|
gameStateManager.rosterManager.recruitUnit = sinon
|
||||||
|
.stub()
|
||||||
|
.resolves(mockRecruitedUnit);
|
||||||
gameStateManager.setGameLoop(mockGameLoop);
|
gameStateManager.setGameLoop(mockGameLoop);
|
||||||
await gameStateManager.init();
|
await gameStateManager.init();
|
||||||
|
|
||||||
|
|
@ -137,7 +140,9 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
mockGameLoop.startLevel = sinon.stub().resolves();
|
mockGameLoop.startLevel = sinon.stub().resolves();
|
||||||
|
|
||||||
// Await the full async chain
|
// 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(
|
expect(gameStateManager.currentState).to.equal(
|
||||||
GameStateManager.STATES.DEPLOYMENT
|
GameStateManager.STATES.DEPLOYMENT
|
||||||
|
|
@ -156,11 +161,15 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
className: "Vanguard",
|
className: "Vanguard",
|
||||||
classId: "CLASS_VANGUARD",
|
classId: "CLASS_VANGUARD",
|
||||||
};
|
};
|
||||||
gameStateManager.rosterManager.recruitUnit = sinon.stub().resolves(mockRecruitedUnit);
|
gameStateManager.rosterManager.recruitUnit = sinon
|
||||||
|
.stub()
|
||||||
|
.resolves(mockRecruitedUnit);
|
||||||
gameStateManager.setGameLoop(mockGameLoop);
|
gameStateManager.setGameLoop(mockGameLoop);
|
||||||
await gameStateManager.init();
|
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();
|
mockGameLoop.startLevel = sinon.stub().resolves();
|
||||||
|
|
||||||
let eventDispatched = false;
|
let eventDispatched = false;
|
||||||
|
|
@ -170,7 +179,9 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
eventData = e.detail.runData;
|
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(eventDispatched).to.be.true;
|
||||||
expect(eventData).to.exist;
|
expect(eventData).to.exist;
|
||||||
|
|
@ -202,7 +213,8 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
await gameStateManager.init();
|
await gameStateManager.init();
|
||||||
|
|
||||||
expect(mockPersistence.loadCampaign.called).to.be.true;
|
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 () => {
|
it("CoA 6: init should handle missing campaign data gracefully", async () => {
|
||||||
|
|
@ -221,9 +233,11 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
mockPersistence.saveCampaign.resetHistory();
|
mockPersistence.saveCampaign.resetHistory();
|
||||||
|
|
||||||
// Dispatch campaign-data-changed event
|
// Dispatch campaign-data-changed event
|
||||||
window.dispatchEvent(new CustomEvent("campaign-data-changed", {
|
window.dispatchEvent(
|
||||||
detail: { missionCompleted: "MISSION_TUTORIAL_01" }
|
new CustomEvent("campaign-data-changed", {
|
||||||
}));
|
detail: { missionCompleted: "MISSION_TUTORIAL_01" },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Wait for async save (event listener is synchronous but save is async)
|
// Wait for async save (event listener is synchronous but save is async)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
@ -233,4 +247,50 @@ describe("Core: GameStateManager (Singleton)", () => {
|
||||||
expect(savedData).to.exist;
|
expect(savedData).to.exist;
|
||||||
expect(savedData.completedMissions).to.be.an("array");
|
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