2025-12-22 20:57:04 +00:00
|
|
|
import { expect } from "@esm-bundle/chai";
|
|
|
|
|
import sinon from "sinon";
|
|
|
|
|
import { MissionManager } from "../../src/managers/MissionManager.js";
|
|
|
|
|
import { narrativeManager } from "../../src/managers/NarrativeManager.js";
|
|
|
|
|
|
|
|
|
|
describe("Manager: MissionManager", () => {
|
|
|
|
|
let manager;
|
|
|
|
|
let mockNarrativeManager;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
manager = new MissionManager();
|
|
|
|
|
|
|
|
|
|
// Mock narrativeManager
|
|
|
|
|
mockNarrativeManager = {
|
|
|
|
|
startSequence: sinon.stub(),
|
|
|
|
|
addEventListener: sinon.stub(),
|
|
|
|
|
removeEventListener: sinon.stub(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Replace the singleton reference in the manager if possible
|
|
|
|
|
// Since it's imported, we'll need to stub the methods we use
|
|
|
|
|
sinon.stub(narrativeManager, "startSequence");
|
|
|
|
|
sinon.stub(narrativeManager, "addEventListener");
|
|
|
|
|
sinon.stub(narrativeManager, "removeEventListener");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
sinon.restore();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 1: Should initialize with tutorial mission registered", () => {
|
|
|
|
|
expect(manager.missionRegistry.has("MISSION_TUTORIAL_01")).to.be.true;
|
|
|
|
|
expect(manager.activeMissionId).to.be.null;
|
|
|
|
|
expect(manager.completedMissions).to.be.instanceof(Set);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 2: registerMission should add mission to registry", () => {
|
|
|
|
|
const newMission = {
|
|
|
|
|
id: "MISSION_TEST_01",
|
|
|
|
|
config: { title: "Test Mission" },
|
|
|
|
|
objectives: { primary: [] },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
manager.registerMission(newMission);
|
|
|
|
|
|
|
|
|
|
expect(manager.missionRegistry.has("MISSION_TEST_01")).to.be.true;
|
|
|
|
|
expect(manager.missionRegistry.get("MISSION_TEST_01")).to.equal(newMission);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 3: getActiveMission should return tutorial if no active mission", () => {
|
|
|
|
|
const mission = manager.getActiveMission();
|
|
|
|
|
|
|
|
|
|
expect(mission).to.exist;
|
|
|
|
|
expect(mission.id).to.equal("MISSION_TUTORIAL_01");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 4: getActiveMission should return active mission if set", () => {
|
|
|
|
|
const testMission = {
|
|
|
|
|
id: "MISSION_TEST_01",
|
|
|
|
|
config: { title: "Test" },
|
|
|
|
|
objectives: { primary: [] },
|
|
|
|
|
};
|
|
|
|
|
manager.registerMission(testMission);
|
|
|
|
|
manager.activeMissionId = "MISSION_TEST_01";
|
|
|
|
|
|
|
|
|
|
const mission = manager.getActiveMission();
|
|
|
|
|
|
|
|
|
|
expect(mission.id).to.equal("MISSION_TEST_01");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 5: setupActiveMission should initialize objectives", () => {
|
|
|
|
|
const mission = manager.getActiveMission();
|
|
|
|
|
mission.objectives = {
|
|
|
|
|
primary: [
|
|
|
|
|
{ type: "ELIMINATE_ALL", target_count: 5 },
|
|
|
|
|
{ type: "ELIMINATE_UNIT", target_def_id: "ENEMY_GOBLIN", target_count: 3 },
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
manager.setupActiveMission();
|
|
|
|
|
|
|
|
|
|
expect(manager.currentObjectives).to.have.length(2);
|
|
|
|
|
expect(manager.currentObjectives[0].current).to.equal(0);
|
|
|
|
|
expect(manager.currentObjectives[0].complete).to.be.false;
|
|
|
|
|
expect(manager.currentObjectives[1].target_count).to.equal(3);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 6: onGameEvent should update ELIMINATE_ALL objectives", () => {
|
|
|
|
|
manager.setupActiveMission();
|
|
|
|
|
manager.currentObjectives = [
|
|
|
|
|
{ type: "ELIMINATE_ALL", target_count: 3, current: 0, complete: false },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_1" });
|
|
|
|
|
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_2" });
|
|
|
|
|
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_3" });
|
|
|
|
|
|
|
|
|
|
expect(manager.currentObjectives[0].current).to.equal(3);
|
|
|
|
|
expect(manager.currentObjectives[0].complete).to.be.true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 7: onGameEvent should update ELIMINATE_UNIT objectives for specific unit", () => {
|
|
|
|
|
manager.setupActiveMission();
|
|
|
|
|
manager.currentObjectives = [
|
|
|
|
|
{
|
|
|
|
|
type: "ELIMINATE_UNIT",
|
|
|
|
|
target_def_id: "ENEMY_GOBLIN",
|
|
|
|
|
target_count: 2,
|
|
|
|
|
current: 0,
|
|
|
|
|
complete: false,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_GOBLIN" });
|
|
|
|
|
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_OTHER" }); // Should not count
|
|
|
|
|
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_GOBLIN" });
|
|
|
|
|
|
|
|
|
|
expect(manager.currentObjectives[0].current).to.equal(2);
|
|
|
|
|
expect(manager.currentObjectives[0].complete).to.be.true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 8: checkVictory should dispatch mission-victory event when all objectives complete", () => {
|
|
|
|
|
const victorySpy = sinon.spy();
|
|
|
|
|
window.addEventListener("mission-victory", victorySpy);
|
|
|
|
|
|
|
|
|
|
manager.setupActiveMission();
|
|
|
|
|
manager.currentObjectives = [
|
|
|
|
|
{ type: "ELIMINATE_ALL", target_count: 2, current: 2, complete: true },
|
|
|
|
|
];
|
|
|
|
|
manager.activeMissionId = "MISSION_TUTORIAL_01";
|
|
|
|
|
|
|
|
|
|
manager.checkVictory();
|
|
|
|
|
|
|
|
|
|
expect(victorySpy.called).to.be.true;
|
|
|
|
|
expect(manager.completedMissions.has("MISSION_TUTORIAL_01")).to.be.true;
|
|
|
|
|
|
|
|
|
|
window.removeEventListener("mission-victory", victorySpy);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 9: completeActiveMission should add mission to completed set", () => {
|
|
|
|
|
manager.activeMissionId = "MISSION_TUTORIAL_01";
|
|
|
|
|
|
|
|
|
|
manager.completeActiveMission();
|
|
|
|
|
|
|
|
|
|
expect(manager.completedMissions.has("MISSION_TUTORIAL_01")).to.be.true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 10: load should restore completed missions", () => {
|
|
|
|
|
const saveData = {
|
|
|
|
|
completedMissions: ["MISSION_TUTORIAL_01", "MISSION_TEST_01"],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
manager.load(saveData);
|
|
|
|
|
|
|
|
|
|
expect(manager.completedMissions.has("MISSION_TUTORIAL_01")).to.be.true;
|
|
|
|
|
expect(manager.completedMissions.has("MISSION_TEST_01")).to.be.true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 11: save should serialize completed missions", () => {
|
|
|
|
|
manager.completedMissions.add("MISSION_TUTORIAL_01");
|
|
|
|
|
manager.completedMissions.add("MISSION_TEST_01");
|
|
|
|
|
|
|
|
|
|
const saved = manager.save();
|
|
|
|
|
|
|
|
|
|
expect(saved.completedMissions).to.be.an("array");
|
|
|
|
|
expect(saved.completedMissions).to.include("MISSION_TUTORIAL_01");
|
|
|
|
|
expect(saved.completedMissions).to.include("MISSION_TEST_01");
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 12: _mapNarrativeIdToFileName should convert narrative IDs to filenames", () => {
|
|
|
|
|
expect(manager._mapNarrativeIdToFileName("NARRATIVE_TUTORIAL_INTRO")).to.equal(
|
|
|
|
|
"tutorial_intro"
|
|
|
|
|
);
|
|
|
|
|
expect(manager._mapNarrativeIdToFileName("NARRATIVE_TUTORIAL_SUCCESS")).to.equal(
|
|
|
|
|
"tutorial_success"
|
|
|
|
|
);
|
|
|
|
|
// The implementation converts NARRATIVE_UNKNOWN to narrative_unknown (lowercase with NARRATIVE_ prefix removed)
|
|
|
|
|
expect(manager._mapNarrativeIdToFileName("NARRATIVE_UNKNOWN")).to.equal("narrative_unknown");
|
|
|
|
|
});
|
2025-12-28 00:54:03 +00:00
|
|
|
|
|
|
|
|
it("CoA 13: getActiveMission should expose enemy_spawns from mission definition", () => {
|
|
|
|
|
const missionWithEnemies = {
|
|
|
|
|
id: "MISSION_TEST",
|
|
|
|
|
config: { title: "Test Mission" },
|
|
|
|
|
enemy_spawns: [
|
|
|
|
|
{ enemy_def_id: "ENEMY_SHARDBORN_SENTINEL", count: 2 },
|
|
|
|
|
],
|
|
|
|
|
objectives: { primary: [] },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
manager.registerMission(missionWithEnemies);
|
|
|
|
|
manager.activeMissionId = "MISSION_TEST";
|
|
|
|
|
|
|
|
|
|
const mission = manager.getActiveMission();
|
|
|
|
|
|
|
|
|
|
expect(mission.enemy_spawns).to.exist;
|
|
|
|
|
expect(mission.enemy_spawns).to.have.length(1);
|
|
|
|
|
expect(mission.enemy_spawns[0].enemy_def_id).to.equal("ENEMY_SHARDBORN_SENTINEL");
|
|
|
|
|
expect(mission.enemy_spawns[0].count).to.equal(2);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("CoA 14: getActiveMission should expose deployment constraints with tutorial hints", () => {
|
|
|
|
|
const missionWithDeployment = {
|
|
|
|
|
id: "MISSION_TEST",
|
|
|
|
|
config: { title: "Test Mission" },
|
|
|
|
|
deployment: {
|
|
|
|
|
suggested_units: ["CLASS_VANGUARD", "CLASS_AETHER_WEAVER"],
|
|
|
|
|
tutorial_hint: "Drag units from the bench to the Green Zone.",
|
|
|
|
|
},
|
|
|
|
|
objectives: { primary: [] },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
manager.registerMission(missionWithDeployment);
|
|
|
|
|
manager.activeMissionId = "MISSION_TEST";
|
|
|
|
|
|
|
|
|
|
const mission = manager.getActiveMission();
|
|
|
|
|
|
|
|
|
|
expect(mission.deployment).to.exist;
|
|
|
|
|
expect(mission.deployment.suggested_units).to.deep.equal([
|
|
|
|
|
"CLASS_VANGUARD",
|
|
|
|
|
"CLASS_AETHER_WEAVER",
|
|
|
|
|
]);
|
|
|
|
|
expect(mission.deployment.tutorial_hint).to.equal(
|
|
|
|
|
"Drag units from the bench to the Green Zone."
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-12-22 20:57:04 +00:00
|
|
|
});
|
|
|
|
|
|