- Introduce the ResearchManager to manage tech trees, node unlocking, and passive effects, enhancing gameplay depth. - Update GameStateManager to integrate the ResearchManager, ensuring seamless data handling for research states. - Implement lazy loading for mission definitions and class data to improve performance and resource management. - Enhance UI components, including the ResearchScreen and MissionBoard, to support new research features and mission prerequisites. - Add comprehensive tests for the ResearchManager and related UI components to validate functionality and integration within the game architecture.
759 lines
25 KiB
JavaScript
759 lines
25 KiB
JavaScript
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", () => {
|
|
const mockUnitManager = {
|
|
activeUnits: new Map([
|
|
["PLAYER_1", { id: "PLAYER_1", team: "PLAYER", currentHealth: 100 }],
|
|
]),
|
|
getUnitsByTeam: sinon.stub().returns([]), // No enemies left
|
|
};
|
|
|
|
manager.setUnitManager(mockUnitManager);
|
|
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", () => {
|
|
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", 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", 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", () => {
|
|
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."
|
|
);
|
|
});
|
|
|
|
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", () => {
|
|
it("CoA 18: Should complete SURVIVE objective when turn count is reached", () => {
|
|
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", () => {
|
|
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 20: Should complete INTERACT objective when unit interacts with target object", () => {
|
|
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", () => {
|
|
// 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);
|
|
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", () => {
|
|
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);
|
|
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", () => {
|
|
it("CoA 23: Should track secondary objectives separately from primary", () => {
|
|
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";
|
|
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", () => {
|
|
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", () => {
|
|
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";
|
|
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;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|