Enhance GameStateManager and Persistence for campaign data management

- Introduce methods for loading and saving campaign data in GameStateManager, improving state management for player progress.
- Update Persistence layer to handle campaign data storage and retrieval, ensuring data integrity and availability.
- Add comprehensive tests for campaign data handling, including scenarios for loading existing data and managing data changes through events.
- Refactor related components to support new campaign features, enhancing overall game state management.
This commit is contained in:
Matthew Mone 2025-12-31 15:06:07 -08:00
parent a9d4064dd8
commit cc38ee2808
4 changed files with 134 additions and 2 deletions

View file

@ -21,6 +21,10 @@ describe("Core: GameStateManager (Singleton)", () => {
loadRun: sinon.stub().resolves(null), loadRun: sinon.stub().resolves(null),
loadRoster: sinon.stub().resolves(null), loadRoster: sinon.stub().resolves(null),
saveRoster: sinon.stub().resolves(), saveRoster: sinon.stub().resolves(),
loadCampaign: sinon.stub().resolves(null),
saveCampaign: sinon.stub().resolves(),
loadMarketState: sinon.stub().resolves(null),
saveMarketState: sinon.stub().resolves(),
}; };
// Inject Mock (replacing the real Persistence instance) // Inject Mock (replacing the real Persistence instance)
gameStateManager.persistence = mockPersistence; gameStateManager.persistence = mockPersistence;
@ -47,6 +51,9 @@ describe("Core: GameStateManager (Singleton)", () => {
objectives: [], objectives: [],
}), }),
playIntro: sinon.stub().resolves(), playIntro: sinon.stub().resolves(),
load: sinon.stub(),
save: sinon.stub().returns({ completedMissions: [] }),
completedMissions: new Set(),
}; };
}); });
@ -146,4 +153,45 @@ describe("Core: GameStateManager (Singleton)", () => {
expect(gameStateManager.activeRunData).to.deep.equal(savedData); expect(gameStateManager.activeRunData).to.deep.equal(savedData);
expect(mockGameLoop.startLevel.calledWith(savedData)).to.be.true; expect(mockGameLoop.startLevel.calledWith(savedData)).to.be.true;
}); });
it("CoA 5: init should load campaign data if available", async () => {
const savedCampaignData = {
completedMissions: ["MISSION_TUTORIAL_01"],
};
mockPersistence.loadCampaign.resolves(savedCampaignData);
await gameStateManager.init();
expect(mockPersistence.loadCampaign.called).to.be.true;
expect(gameStateManager.missionManager.load.calledWith(savedCampaignData)).to.be.true;
});
it("CoA 6: init should handle missing campaign data gracefully", async () => {
mockPersistence.loadCampaign.resolves(null);
await gameStateManager.init();
expect(mockPersistence.loadCampaign.called).to.be.true;
expect(gameStateManager.missionManager.load.called).to.be.false;
});
it("CoA 7: campaign-data-changed event should trigger save", async () => {
await gameStateManager.init();
// Clear any previous calls
mockPersistence.saveCampaign.resetHistory();
// Dispatch campaign-data-changed event
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));
expect(mockPersistence.saveCampaign.called).to.be.true;
const savedData = mockPersistence.saveCampaign.firstCall.args[0];
expect(savedData).to.exist;
expect(savedData.completedMissions).to.be.an("array");
});
}); });

View file

@ -18,6 +18,10 @@ describe("Core: GameStateManager - Hub Integration", () => {
loadRun: sinon.stub().resolves(null), loadRun: sinon.stub().resolves(null),
loadRoster: sinon.stub().resolves(null), loadRoster: sinon.stub().resolves(null),
saveRoster: sinon.stub().resolves(), saveRoster: sinon.stub().resolves(),
loadCampaign: sinon.stub().resolves(null),
saveCampaign: sinon.stub().resolves(),
loadMarketState: sinon.stub().resolves(null),
saveMarketState: sinon.stub().resolves(),
}; };
gameStateManager.persistence = mockPersistence; gameStateManager.persistence = mockPersistence;
@ -41,8 +45,10 @@ describe("Core: GameStateManager - Hub Integration", () => {
objectives: [], objectives: [],
}), }),
playIntro: sinon.stub().resolves(), playIntro: sinon.stub().resolves(),
completedMissions: new Set(),
missionRegistry: new Map(), missionRegistry: new Map(),
load: sinon.stub(),
save: sinon.stub().returns({ completedMissions: [] }),
completedMissions: new Set(),
}; };
}); });

View file

@ -80,11 +80,13 @@ describe("Core: Persistence", () => {
await initPromise; await initPromise;
expect(globalObj.indexedDB.open.calledWith("AetherShardsDB", 2)).to.be.true; expect(globalObj.indexedDB.open.calledWith("AetherShardsDB", 4)).to.be.true;
expect(mockDB.createObjectStore.calledWith("Runs", { keyPath: "id" })).to.be expect(mockDB.createObjectStore.calledWith("Runs", { keyPath: "id" })).to.be
.true; .true;
expect(mockDB.createObjectStore.calledWith("Roster", { keyPath: "id" })).to expect(mockDB.createObjectStore.calledWith("Roster", { keyPath: "id" })).to
.be.true; .be.true;
expect(mockDB.createObjectStore.calledWith("Campaign", { keyPath: "id" }))
.to.be.true;
expect(persistence.db).to.equal(mockDB); expect(persistence.db).to.equal(mockDB);
}); });
@ -243,4 +245,72 @@ describe("Core: Persistence", () => {
expect(persistence.db).to.equal(mockDB); expect(persistence.db).to.equal(mockDB);
expect(mockStore.put.calledOnce).to.be.true; expect(mockStore.put.calledOnce).to.be.true;
}); });
it("CoA 9: saveCampaign should store campaign data with campaign_data id", async () => {
persistence.db = mockDB;
const campaignData = {
completedMissions: ["MISSION_TUTORIAL_01", "MISSION_TEST_01"],
};
const mockPutRequest = {
onsuccess: null,
onerror: null,
};
mockStore.put.returns(mockPutRequest);
const savePromise = persistence.saveCampaign(campaignData);
mockPutRequest.onsuccess();
await savePromise;
expect(mockDB.transaction.calledWith(["Campaign"], "readwrite")).to.be.true;
expect(mockStore.put.calledOnce).to.be.true;
const savedData = mockStore.put.firstCall.args[0];
expect(savedData.id).to.equal("campaign_data");
expect(savedData.data).to.deep.equal(campaignData);
});
it("CoA 10: loadCampaign should extract data from stored object", async () => {
persistence.db = mockDB;
const storedData = {
id: "campaign_data",
data: {
completedMissions: ["MISSION_TUTORIAL_01", "MISSION_TEST_01"],
},
};
const mockGetRequest = {
onsuccess: null,
onerror: null,
result: storedData,
};
mockStore.get.returns(mockGetRequest);
const loadPromise = persistence.loadCampaign();
mockGetRequest.onsuccess();
const result = await loadPromise;
expect(mockDB.transaction.calledWith(["Campaign"], "readonly")).to.be.true;
expect(mockStore.get.calledWith("campaign_data")).to.be.true;
expect(result).to.deep.equal(storedData.data);
});
it("CoA 11: loadCampaign should return null if no data exists", async () => {
persistence.db = mockDB;
const mockGetRequest = {
onsuccess: null,
onerror: null,
result: undefined,
};
mockStore.get.returns(mockGetRequest);
const loadPromise = persistence.loadCampaign();
mockGetRequest.onsuccess();
const result = await loadPromise;
expect(result).to.be.null;
});
}); });

View file

@ -156,9 +156,17 @@ describe("Manager: MissionManager", () => {
rewards: { guaranteed: {} }, rewards: { guaranteed: {} },
}; };
// Spy on window.dispatchEvent to verify campaign-data-changed event
const eventSpy = sinon.spy();
window.addEventListener("campaign-data-changed", eventSpy);
await manager.completeActiveMission(); await manager.completeActiveMission();
expect(manager.completedMissions.has("MISSION_TUTORIAL_01")).to.be.true; expect(manager.completedMissions.has("MISSION_TUTORIAL_01")).to.be.true;
expect(eventSpy.called).to.be.true;
expect(eventSpy.firstCall.args[0].detail.missionCompleted).to.equal("MISSION_TUTORIAL_01");
window.removeEventListener("campaign-data-changed", eventSpy);
}); });
it("CoA 10: load should restore completed missions", () => { it("CoA 10: load should restore completed missions", () => {