aether-shards/test/managers/MissionManager.test.js
Matthew Mone 45276d1bd4 Implement BarracksScreen for roster management and enhance game state integration
- Introduce the BarracksScreen component to manage the player roster, allowing for unit selection, healing, and dismissal.
- Update GameStateManager to support roster persistence and integrate with the new BarracksScreen for seamless data handling.
- Enhance UI components for improved user experience, including dynamic filtering and sorting of units.
- Implement detailed unit information display and actions for healing and dismissing units.
- Add comprehensive tests for the BarracksScreen to validate functionality and integration with the overall game architecture.
2025-12-31 20:11:00 -08:00

712 lines
23 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", () => {
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: {} },
};
// 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");
});
});
});