Introduce the HubScreen as the main interface for managing resources, units, and mission selection, integrating with the GameStateManager for dynamic data binding. Implement the MissionBoard component to display and select available missions, enhancing user interaction with mission details and selection logic. Update the GameStateManager to handle transitions between game states, ensuring a seamless experience for players. Add tests for HubScreen and MissionBoard to validate functionality and integration with the overall game architecture.
702 lines
22 KiB
JavaScript
702 lines
22 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;
|
|
|
|
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 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: {} },
|
|
};
|
|
|
|
await 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");
|
|
});
|
|
|
|
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 localStorage", () => {
|
|
manager.unlockClasses(["CLASS_TINKER", "CLASS_SAPPER"]);
|
|
|
|
const stored = localStorage.getItem("aether_shards_unlocks");
|
|
expect(stored).to.exist;
|
|
|
|
const unlocks = JSON.parse(stored);
|
|
expect(unlocks).to.include("CLASS_TINKER");
|
|
expect(unlocks).to.include("CLASS_SAPPER");
|
|
});
|
|
|
|
it("CoA 28: Should merge new unlocks with existing unlocks", () => {
|
|
localStorage.setItem(
|
|
"aether_shards_unlocks",
|
|
JSON.stringify(["CLASS_VANGUARD"])
|
|
);
|
|
|
|
manager.unlockClasses(["CLASS_TINKER"]);
|
|
|
|
const unlocks = JSON.parse(
|
|
localStorage.getItem("aether_shards_unlocks")
|
|
);
|
|
expect(unlocks).to.include("CLASS_VANGUARD");
|
|
expect(unlocks).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");
|
|
});
|
|
});
|
|
});
|
|
|