aether-shards/test/managers/MissionManager.test.js

886 lines
29 KiB
JavaScript
Raw Normal View History

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;
let mockPersistence;
beforeEach(() => {
// Create mock persistence
mockPersistence = {
loadUnlocks: sinon.stub().resolves([]),
saveUnlocks: sinon.stub().resolves(),
};
manager = new MissionManager(mockPersistence);
// 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", async () => {
await manager._ensureMissionsLoaded();
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", async () => {
await manager._ensureMissionsLoaded();
const mission = await manager.getActiveMission();
expect(mission).to.exist;
expect(mission.id).to.equal("MISSION_TUTORIAL_01");
});
it("CoA 4: getActiveMission should return active mission if set", async () => {
const testMission = {
id: "MISSION_TEST_01",
config: { title: "Test" },
objectives: { primary: [] },
};
manager.registerMission(testMission);
manager.activeMissionId = "MISSION_TEST_01";
const mission = await manager.getActiveMission();
expect(mission.id).to.equal("MISSION_TEST_01");
});
it("CoA 5: setupActiveMission should initialize objectives", async () => {
await manager._ensureMissionsLoaded();
const mission = await manager.getActiveMission();
mission.objectives = {
primary: [
{ type: "ELIMINATE_ALL", target_count: 5 },
{ type: "ELIMINATE_UNIT", target_def_id: "ENEMY_GOBLIN", target_count: 3 },
],
};
await 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 complete ELIMINATE_ALL when all enemies are dead", async () => {
await manager._ensureMissionsLoaded();
const mockUnitManager = {
activeUnits: new Map([
["PLAYER_1", { id: "PLAYER_1", team: "PLAYER", currentHealth: 100 }],
]),
getUnitsByTeam: sinon.stub().returns([]), // No enemies left
};
manager.setUnitManager(mockUnitManager);
await manager.setupActiveMission();
manager.currentObjectives = [
{ type: "ELIMINATE_ALL", complete: false },
];
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_1" });
expect(manager.currentObjectives[0].complete).to.be.true;
});
it("CoA 7: onGameEvent should update ELIMINATE_UNIT objectives for specific unit", async () => {
await manager._ensureMissionsLoaded();
await 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", defId: "ENEMY_GOBLIN" });
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_OTHER", defId: "ENEMY_OTHER" }); // Should not count
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_GOBLIN", defId: "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", async () => {
await manager._ensureMissionsLoaded();
const victorySpy = sinon.spy();
window.addEventListener("mission-victory", victorySpy);
// Stub completeActiveMission to avoid async issues
sinon.stub(manager, "completeActiveMission").resolves();
await manager.setupActiveMission();
manager.currentObjectives = [
{ type: "ELIMINATE_ALL", target_count: 2, current: 2, complete: true },
];
manager.activeMissionId = "MISSION_TUTORIAL_01";
manager.currentMissionDef = {
id: "MISSION_TUTORIAL_01",
rewards: { guaranteed: {} },
};
manager.checkVictory();
expect(victorySpy.called).to.be.true;
expect(manager.completeActiveMission.called).to.be.true;
window.removeEventListener("mission-victory", victorySpy);
});
it("CoA 9: completeActiveMission should add mission to completed set", async () => {
manager.activeMissionId = "MISSION_TUTORIAL_01";
manager.currentMissionDef = {
id: "MISSION_TUTORIAL_01",
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();
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", () => {
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");
});
it("CoA 13: getActiveMission should expose enemy_spawns from mission definition", async () => {
await manager._ensureMissionsLoaded();
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 = await 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 mission_objects from mission definition", async () => {
await manager._ensureMissionsLoaded();
const missionWithObjects = {
id: "MISSION_TEST",
config: { title: "Test Mission" },
mission_objects: [
{
object_id: "OBJ_SIGNAL_RELAY",
placement_strategy: "center_of_enemy_room",
},
{
object_id: "OBJ_DATA_TERMINAL",
position: { x: 10, y: 1, z: 10 },
},
],
objectives: { primary: [] },
};
manager.registerMission(missionWithObjects);
manager.activeMissionId = "MISSION_TEST";
const mission = await manager.getActiveMission();
expect(mission.mission_objects).to.exist;
expect(mission.mission_objects).to.have.length(2);
expect(mission.mission_objects[0].object_id).to.equal("OBJ_SIGNAL_RELAY");
expect(mission.mission_objects[0].placement_strategy).to.equal("center_of_enemy_room");
expect(mission.mission_objects[1].object_id).to.equal("OBJ_DATA_TERMINAL");
expect(mission.mission_objects[1].position).to.deep.equal({ x: 10, y: 1, z: 10 });
});
it("CoA 15: getActiveMission should expose deployment constraints with tutorial hints", async () => {
await manager._ensureMissionsLoaded();
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 = await 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."
);
});
describe("Failure Conditions", () => {
it("CoA 15: Should trigger SQUAD_WIPE failure when all player units die", () => {
const failureSpy = sinon.spy();
window.addEventListener("mission-failure", failureSpy);
const mockUnitManager = {
activeUnits: new Map(), // No player units alive
getUnitById: sinon.stub(),
getUnitsByTeam: sinon.stub().returns([]), // No player units
};
manager.setUnitManager(mockUnitManager);
manager.setupActiveMission();
manager.failureConditions = [{ type: "SQUAD_WIPE" }];
manager.checkFailureConditions("PLAYER_DEATH", { unitId: "PLAYER_1" });
expect(failureSpy.called).to.be.true;
expect(failureSpy.firstCall.args[0].detail.reason).to.equal("SQUAD_WIPE");
window.removeEventListener("mission-failure", failureSpy);
});
it("CoA 16: Should trigger VIP_DEATH failure when VIP unit dies", () => {
const failureSpy = sinon.spy();
window.addEventListener("mission-failure", failureSpy);
const mockUnit = {
id: "VIP_1",
team: "PLAYER",
tags: ["VIP_ESCORT"],
};
const mockUnitManager = {
getUnitById: sinon.stub().returns(mockUnit),
};
manager.setUnitManager(mockUnitManager);
manager.failureConditions = [
{ type: "VIP_DEATH", target_tag: "VIP_ESCORT" },
];
manager.checkFailureConditions("PLAYER_DEATH", { unitId: "VIP_1" });
expect(failureSpy.called).to.be.true;
expect(failureSpy.firstCall.args[0].detail.reason).to.equal("VIP_DEATH");
window.removeEventListener("mission-failure", failureSpy);
});
it("CoA 17: Should trigger TURN_LIMIT_EXCEEDED failure when turn limit is exceeded", () => {
const failureSpy = sinon.spy();
window.addEventListener("mission-failure", failureSpy);
manager.currentTurn = 11;
manager.failureConditions = [{ type: "TURN_LIMIT_EXCEEDED", turn_limit: 10 }];
manager.checkFailureConditions("TURN_END", {});
expect(failureSpy.called).to.be.true;
expect(failureSpy.firstCall.args[0].detail.reason).to.equal(
"TURN_LIMIT_EXCEEDED"
);
window.removeEventListener("mission-failure", failureSpy);
});
});
describe("Additional Objective Types", () => {
beforeEach(async () => {
await manager._ensureMissionsLoaded();
});
it("CoA 18: Should complete SURVIVE objective when turn count is reached", async () => {
await manager.setupActiveMission();
manager.currentObjectives = [
{
type: "SURVIVE",
turn_count: 5,
current: 0,
complete: false,
},
];
manager.currentTurn = 0;
manager.updateTurn(5);
manager.onGameEvent("TURN_END", { turn: 5 });
expect(manager.currentObjectives[0].complete).to.be.true;
});
it("CoA 19: Should complete REACH_ZONE objective when unit reaches target zone", async () => {
await manager.setupActiveMission();
manager.currentObjectives = [
{
type: "REACH_ZONE",
zone_coords: [{ x: 5, y: 0, z: 5 }],
complete: false,
},
];
manager.onGameEvent("UNIT_MOVE", {
position: { x: 5, y: 0, z: 5 },
});
expect(manager.currentObjectives[0].complete).to.be.true;
});
it("CoA 19b: Should track progress for REACH_ZONE objective with multiple zones", async () => {
await manager.setupActiveMission();
manager.currentObjectives = [
{
type: "REACH_ZONE",
target_count: 3,
zone_coords: [
{ x: 5, y: 0, z: 5 },
{ x: 10, y: 0, z: 10 },
{ x: 15, y: 0, z: 15 },
],
current: 0,
complete: false,
},
];
// Reach first zone
manager.onGameEvent("UNIT_MOVE", {
position: { x: 5, y: 0, z: 5 },
});
expect(manager.currentObjectives[0].current).to.equal(1);
expect(manager.currentObjectives[0].complete).to.be.false;
expect(manager.currentObjectives[0].zone_coords.length).to.equal(2);
// Reach second zone
manager.onGameEvent("UNIT_MOVE", {
position: { x: 10, y: 0, z: 10 },
});
expect(manager.currentObjectives[0].current).to.equal(2);
expect(manager.currentObjectives[0].complete).to.be.false;
expect(manager.currentObjectives[0].zone_coords.length).to.equal(1);
// Reach third zone - should complete
manager.onGameEvent("UNIT_MOVE", {
position: { x: 15, y: 0, z: 15 },
});
expect(manager.currentObjectives[0].current).to.equal(3);
expect(manager.currentObjectives[0].complete).to.be.true;
expect(manager.currentObjectives[0].zone_coords.length).to.equal(0);
});
it("CoA 19c: Should handle Y-level variance when matching zones (for teleport)", async () => {
await manager.setupActiveMission();
manager.currentObjectives = [
{
type: "REACH_ZONE",
zone_coords: [{ x: 5, y: 1, z: 5 }],
complete: false,
},
];
// Teleport might place unit at slightly different Y level
manager.onGameEvent("UNIT_MOVE", {
position: { x: 5, y: 0, z: 5 }, // Y differs by 1
});
expect(manager.currentObjectives[0].complete).to.be.true;
});
it("CoA 19d: Should complete REACH_ZONE objective when teleporting to target zone", async () => {
await manager.setupActiveMission();
manager.currentObjectives = [
{
type: "REACH_ZONE",
target_count: 1,
zone_coords: [{ x: 10, y: 1, z: 10 }],
current: 0,
complete: false,
},
];
// Simulate teleport to zone (UNIT_MOVE event with position matching zone)
manager.onGameEvent("UNIT_MOVE", {
unitId: "UNIT_TEST",
position: { x: 10, y: 1, z: 10 },
});
expect(manager.currentObjectives[0].complete).to.be.true;
expect(manager.currentObjectives[0].current).to.equal(1);
});
it("CoA 20: Should complete INTERACT objective when unit interacts with target object", async () => {
await manager.setupActiveMission();
manager.currentObjectives = [
{
type: "INTERACT",
target_object_id: "OBJECT_LEVER",
complete: false,
},
];
manager.onGameEvent("INTERACT", { objectId: "OBJECT_LEVER" });
expect(manager.currentObjectives[0].complete).to.be.true;
});
it("CoA 21: Should complete ELIMINATE_ALL when all enemies are eliminated", async () => {
// Mock UnitManager with only player units (no enemies)
const mockUnitManager = {
activeUnits: new Map([
["PLAYER_1", { id: "PLAYER_1", team: "PLAYER", currentHealth: 100 }],
]),
};
manager.setUnitManager(mockUnitManager);
await manager.setupActiveMission();
manager.currentObjectives = [
{
type: "ELIMINATE_ALL",
complete: false,
},
];
manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_1" });
expect(manager.currentObjectives[0].complete).to.be.true;
});
it("CoA 22: Should complete SQUAD_SURVIVAL objective when minimum units survive", async () => {
const mockUnitManager = {
activeUnits: new Map([
["PLAYER_1", { id: "PLAYER_1", team: "PLAYER", currentHealth: 100 }],
["PLAYER_2", { id: "PLAYER_2", team: "PLAYER", currentHealth: 100 }],
["PLAYER_3", { id: "PLAYER_3", team: "PLAYER", currentHealth: 100 }],
["PLAYER_4", { id: "PLAYER_4", team: "PLAYER", currentHealth: 100 }],
]),
getUnitsByTeam: sinon.stub().returns([
{ id: "PLAYER_1", team: "PLAYER", currentHealth: 100 },
{ id: "PLAYER_2", team: "PLAYER", currentHealth: 100 },
{ id: "PLAYER_3", team: "PLAYER", currentHealth: 100 },
{ id: "PLAYER_4", team: "PLAYER", currentHealth: 100 },
]),
};
manager.setUnitManager(mockUnitManager);
await manager.setupActiveMission();
manager.currentObjectives = [
{
type: "SQUAD_SURVIVAL",
min_alive: 4,
complete: false,
},
];
manager.onGameEvent("PLAYER_DEATH", { unitId: "PLAYER_5" });
expect(manager.currentObjectives[0].complete).to.be.true;
});
});
describe("Secondary Objectives", () => {
beforeEach(async () => {
await manager._ensureMissionsLoaded();
});
it("CoA 23: Should track secondary objectives separately from primary", async () => {
const mission = {
id: "MISSION_TEST",
config: { title: "Test" },
objectives: {
primary: [{ type: "ELIMINATE_ALL", id: "PRIMARY_1" }],
secondary: [
{ type: "SURVIVE", turn_count: 10, id: "SECONDARY_1" },
],
},
};
manager.registerMission(mission);
manager.activeMissionId = "MISSION_TEST";
await manager.setupActiveMission();
expect(manager.currentObjectives).to.have.length(1);
expect(manager.secondaryObjectives).to.have.length(1);
expect(manager.secondaryObjectives[0].id).to.equal("SECONDARY_1");
});
it("CoA 24: Should update secondary objectives on game events", async () => {
await manager.setupActiveMission();
manager.secondaryObjectives = [
{
type: "SURVIVE",
turn_count: 3,
current: 0,
complete: false,
},
];
manager.currentTurn = 0;
manager.updateTurn(3);
manager.onGameEvent("TURN_END", { turn: 3 });
expect(manager.secondaryObjectives[0].complete).to.be.true;
});
});
describe("Reward Distribution", () => {
beforeEach(() => {
// Clear localStorage before each test
localStorage.clear();
});
it("CoA 25: Should distribute guaranteed rewards on mission completion", () => {
const rewardSpy = sinon.spy();
window.addEventListener("mission-rewards", rewardSpy);
manager.activeMissionId = "MISSION_TEST";
manager.currentMissionDef = {
id: "MISSION_TEST",
rewards: {
guaranteed: {
xp: 500,
currency: { aether_shards: 200 },
items: ["ITEM_ELITE_BLAST_PLATE"],
unlocks: ["CLASS_SAPPER"],
},
},
};
manager.distributeRewards();
expect(rewardSpy.called).to.be.true;
const rewardData = rewardSpy.firstCall.args[0].detail;
expect(rewardData.xp).to.equal(500);
expect(rewardData.currency.aether_shards).to.equal(200);
expect(rewardData.items).to.include("ITEM_ELITE_BLAST_PLATE");
expect(rewardData.unlocks).to.include("CLASS_SAPPER");
window.removeEventListener("mission-rewards", rewardSpy);
});
it("CoA 26: Should distribute conditional rewards for completed secondary objectives", () => {
const rewardSpy = sinon.spy();
window.addEventListener("mission-rewards", rewardSpy);
manager.activeMissionId = "MISSION_TEST";
manager.currentMissionDef = {
id: "MISSION_TEST",
rewards: {
guaranteed: { xp: 100 },
conditional: [
{
objective_id: "OBJ_TIME_LIMIT",
reward: { currency: { aether_shards: 100 } },
},
],
},
};
manager.secondaryObjectives = [
{
id: "OBJ_TIME_LIMIT",
complete: true,
},
];
manager.distributeRewards();
const rewardData = rewardSpy.firstCall.args[0].detail;
expect(rewardData.currency.aether_shards).to.equal(100);
window.removeEventListener("mission-rewards", rewardSpy);
});
it("CoA 27: Should unlock classes and store in IndexedDB", async () => {
await manager.unlockClasses(["CLASS_TINKER", "CLASS_SAPPER"]);
expect(mockPersistence.saveUnlocks.calledOnce).to.be.true;
const savedUnlocks = mockPersistence.saveUnlocks.firstCall.args[0];
expect(savedUnlocks).to.include("CLASS_TINKER");
expect(savedUnlocks).to.include("CLASS_SAPPER");
});
it("CoA 28: Should merge new unlocks with existing unlocks", async () => {
// Set up existing unlocks
mockPersistence.loadUnlocks.resolves(["CLASS_VANGUARD"]);
await manager.unlockClasses(["CLASS_TINKER"]);
expect(mockPersistence.saveUnlocks.calledOnce).to.be.true;
const savedUnlocks = mockPersistence.saveUnlocks.firstCall.args[0];
expect(savedUnlocks).to.include("CLASS_VANGUARD");
expect(savedUnlocks).to.include("CLASS_TINKER");
});
it("CoA 29: Should distribute faction reputation rewards", () => {
const rewardSpy = sinon.spy();
window.addEventListener("mission-rewards", rewardSpy);
manager.activeMissionId = "MISSION_TEST";
manager.currentMissionDef = {
id: "MISSION_TEST",
rewards: {
guaranteed: {},
faction_reputation: {
IRON_LEGION: 50,
COGWORK_CONCORD: -10,
},
},
};
manager.distributeRewards();
const rewardData = rewardSpy.firstCall.args[0].detail;
expect(rewardData.factionReputation.IRON_LEGION).to.equal(50);
expect(rewardData.factionReputation.COGWORK_CONCORD).to.equal(-10);
window.removeEventListener("mission-rewards", rewardSpy);
});
});
describe("Mission Conclusion", () => {
it("CoA 30: completeActiveMission should mark mission as completed", async () => {
manager.activeMissionId = "MISSION_TEST";
manager.currentMissionDef = {
id: "MISSION_TEST",
rewards: { guaranteed: {} },
};
await manager.completeActiveMission();
expect(manager.completedMissions.has("MISSION_TEST")).to.be.true;
});
it("CoA 31: completeActiveMission should play outro narrative if available", async () => {
const outroPromise = Promise.resolve();
sinon.stub(manager, "playOutro").returns(outroPromise);
manager.activeMissionId = "MISSION_TEST";
manager.currentMissionDef = {
id: "MISSION_TEST",
narrative: { outro_success: "NARRATIVE_TUTORIAL_SUCCESS" },
rewards: { guaranteed: {} },
};
await manager.completeActiveMission();
expect(manager.playOutro.calledWith("NARRATIVE_TUTORIAL_SUCCESS")).to.be
.true;
});
it("CoA 32: checkVictory should not trigger if not all primary objectives complete", () => {
const victorySpy = sinon.spy();
window.addEventListener("mission-victory", victorySpy);
manager.setupActiveMission();
manager.currentObjectives = [
{ type: "ELIMINATE_ALL", complete: true },
{ type: "SURVIVE", turn_count: 5, complete: false },
];
manager.activeMissionId = "MISSION_TEST";
manager.checkVictory();
expect(victorySpy.called).to.be.false;
window.removeEventListener("mission-victory", victorySpy);
});
it("CoA 33: checkVictory should include objective data in victory event", async () => {
const victorySpy = sinon.spy();
window.addEventListener("mission-victory", victorySpy);
// Stub completeActiveMission to avoid async issues
sinon.stub(manager, "completeActiveMission").resolves();
manager.setupActiveMission();
manager.currentObjectives = [
{ type: "ELIMINATE_ALL", complete: true, id: "OBJ_1" },
];
manager.secondaryObjectives = [
{ type: "SURVIVE", complete: true, id: "OBJ_2" },
];
manager.activeMissionId = "MISSION_TEST";
manager.currentMissionDef = {
id: "MISSION_TEST",
rewards: { guaranteed: {} },
};
manager.checkVictory();
expect(victorySpy.called).to.be.true;
const detail = victorySpy.firstCall.args[0].detail;
expect(detail.primaryObjectives).to.exist;
expect(detail.secondaryObjectives).to.exist;
window.removeEventListener("mission-victory", victorySpy);
});
});
describe("UnitManager and TurnSystem Integration", () => {
it("CoA 34: setUnitManager should store UnitManager reference", () => {
const mockUnitManager = { activeUnits: new Map() };
manager.setUnitManager(mockUnitManager);
expect(manager.unitManager).to.equal(mockUnitManager);
});
it("CoA 35: setTurnSystem should store TurnSystem reference", () => {
const mockTurnSystem = { round: 1 };
manager.setTurnSystem(mockTurnSystem);
expect(manager.turnSystem).to.equal(mockTurnSystem);
});
it("CoA 36: updateTurn should update current turn count", () => {
manager.updateTurn(5);
expect(manager.currentTurn).to.equal(5);
manager.updateTurn(10);
expect(manager.currentTurn).to.equal(10);
});
it("CoA 37: setupActiveMission should initialize failure conditions", async () => {
await manager._ensureMissionsLoaded();
const mission = {
id: "MISSION_TEST",
config: { title: "Test" },
objectives: {
primary: [],
failure_conditions: [
{ type: "SQUAD_WIPE" },
{ type: "VIP_DEATH", target_tag: "VIP_ESCORT" },
],
},
};
manager.registerMission(mission);
manager.activeMissionId = "MISSION_TEST";
await manager.setupActiveMission();
expect(manager.failureConditions).to.have.length(2);
expect(manager.failureConditions[0].type).to.equal("SQUAD_WIPE");
expect(manager.failureConditions[1].type).to.equal("VIP_DEATH");
});
});
describe("Lazy Loading", () => {
it("CoA 31: Should lazy-load missions on first access", async () => {
// Create a fresh manager to test lazy loading
const freshManager = new MissionManager(mockPersistence);
// Initially, registry should be empty (missions not loaded)
expect(freshManager.missionRegistry.size).to.equal(0);
// Trigger lazy loading
await freshManager._ensureMissionsLoaded();
// Now missions should be loaded
expect(freshManager.missionRegistry.size).to.be.greaterThan(0);
expect(freshManager.missionRegistry.has("MISSION_TUTORIAL_01")).to.be.true;
});
it("CoA 32: Should not reload missions if already loaded", async () => {
// Load missions first time
await manager._ensureMissionsLoaded();
const firstSize = manager.missionRegistry.size;
// Load again - should not duplicate
await manager._ensureMissionsLoaded();
const secondSize = manager.missionRegistry.size;
expect(firstSize).to.equal(secondSize);
});
it("CoA 33: Should handle lazy loading errors gracefully", async () => {
// Create a manager with a failing persistence (if needed)
const freshManager = new MissionManager(mockPersistence);
// Should not throw even if missions fail to load
try {
await freshManager._ensureMissionsLoaded();
// If we get here, it handled gracefully
expect(true).to.be.true;
} catch (error) {
// If error occurs, it should be handled
expect(error).to.exist;
}
});
});
});