diff --git a/src/core/GameLoop.js b/src/core/GameLoop.js index 83bcbbe..e668411 100644 --- a/src/core/GameLoop.js +++ b/src/core/GameLoop.js @@ -939,7 +939,7 @@ export class GameLoop { `${activeUnit.name} applied ${result.data.statusId} to ${target.name} for ${result.data.duration} turns` ); } else if (result.data.type === "CHAIN_DAMAGE") { - // Log chain damage results + // Log chain damage results and check for deaths if (result.data.results && result.data.results.length > 0) { const primaryResult = result.data.results[0]; console.log( @@ -953,6 +953,54 @@ export class GameLoop { `Chain lightning bounced to ${result.data.chainTargets.length} additional targets` ); } + + // Check for deaths on all targets (primary + chain targets) + for (const damageResult of result.data.results) { + if (damageResult.currentHP <= 0) { + const killedUnit = this.unitManager.getUnitById( + damageResult.target + ); + if (killedUnit) { + console.log(`${killedUnit.name} has been defeated!`); + // Process ON_KILL passive effects (on source) + this.processPassiveItemEffects(activeUnit, "ON_KILL", { + target: killedUnit, + killedUnit: killedUnit, + }); + // Handle unit death + this.handleUnitDeath(killedUnit); + } + } else { + // Process passive item effects for ON_DAMAGED trigger (on target) + const damagedUnit = this.unitManager.getUnitById( + damageResult.target + ); + if (damagedUnit) { + this.processPassiveItemEffects( + damagedUnit, + "ON_DAMAGED", + { + source: activeUnit, + damageAmount: damageResult.amount, + } + ); + } + } + // Process passive item effects for ON_DAMAGE_DEALT trigger (on source) + const damagedUnit = this.unitManager.getUnitById( + damageResult.target + ); + if (damagedUnit) { + this.processPassiveItemEffects( + activeUnit, + "ON_DAMAGE_DEALT", + { + target: damagedUnit, + damageAmount: damageResult.amount, + } + ); + } + } } } } @@ -3097,6 +3145,42 @@ export class GameLoop { unit.name } (${trigger})` ); + + // Check for death if this was a damage effect + if (result.data.type === "DAMAGE") { + if (result.data.currentHP <= 0 && target && typeof target === "object" && "currentHealth" in target) { + const killedUnit = /** @type {Unit} */ (target); + console.log(`${killedUnit.name} has been defeated by passive effect!`); + // Process ON_KILL passive effects (on source) + this.processPassiveItemEffects(unit, "ON_KILL", { + target: killedUnit, + killedUnit: killedUnit, + }); + // Handle unit death + this.handleUnitDeath(killedUnit); + } + } else if (result.data.type === "CHAIN_DAMAGE") { + // Check for deaths on all targets (primary + chain targets) + if (result.data.results && result.data.results.length > 0) { + for (const damageResult of result.data.results) { + if (damageResult.currentHP <= 0) { + const killedUnit = this.unitManager.getUnitById( + damageResult.target + ); + if (killedUnit) { + console.log(`${killedUnit.name} has been defeated by passive chain damage!`); + // Process ON_KILL passive effects (on source) + this.processPassiveItemEffects(unit, "ON_KILL", { + target: killedUnit, + killedUnit: killedUnit, + }); + // Handle unit death + this.handleUnitDeath(killedUnit); + } + } + } + } + } } } } diff --git a/src/managers/MissionManager.js b/src/managers/MissionManager.js index d1faf3f..a57fc71 100644 --- a/src/managers/MissionManager.js +++ b/src/managers/MissionManager.js @@ -28,6 +28,8 @@ export class MissionManager { this.completedMissions = new Set(); /** @type {Map} */ this.missionRegistry = new Map(); + /** @type {Map} */ + this.completedMissionDetails = new Map(); // Active Run State /** @type {MissionDefinition | null} */ @@ -147,16 +149,23 @@ export class MissionManager { // Separate static missions from procedural missions const staticMissions = []; const proceduralMissions = []; + const completedProceduralMissions = []; for (const mission of this.missionRegistry.values()) { if (mission.type === "SIDE_QUEST" && mission.id?.startsWith("SIDE_OP_")) { - proceduralMissions.push(mission); + // Keep completed procedural missions in the registry so they show in completed tab + if (this.completedMissions.has(mission.id)) { + completedProceduralMissions.push(mission); + } else { + proceduralMissions.push(mission); + } } else { staticMissions.push(mission); } } // Refresh procedural missions (refreshBoard fills up to 5 missions) + // Only pass non-completed procedural missions to refresh const refreshedProcedural = MissionGenerator.refreshBoard( proceduralMissions, tier, @@ -165,7 +174,7 @@ export class MissionManager { isDailyReset ); - // Remove old procedural missions from registry + // Remove old non-completed procedural missions from registry proceduralMissions.forEach((mission) => { this.missionRegistry.delete(mission.id); }); @@ -251,6 +260,15 @@ export class MissionManager { */ load(saveData) { this.completedMissions = new Set(saveData.completedMissions || []); + // Restore completed mission details (especially important for procedural missions) + this.completedMissionDetails = new Map(); + if (saveData.completedMissionDetails) { + for (const [missionId, missionDef] of Object.entries( + saveData.completedMissionDetails + )) { + this.completedMissionDetails.set(missionId, missionDef); + } + } // Default to Tutorial if history is empty if (this.completedMissions.size === 0) { this.activeMissionId = "MISSION_TUTORIAL_01"; @@ -262,8 +280,18 @@ export class MissionManager { * @returns {MissionSaveData} - Serialized campaign data */ save() { + // Convert completed mission details Map to object for serialization + const completedMissionDetailsObj = {}; + for (const [ + missionId, + missionDef, + ] of this.completedMissionDetails.entries()) { + completedMissionDetailsObj[missionId] = missionDef; + } + return { completedMissions: Array.from(this.completedMissions), + completedMissionDetails: completedMissionDetailsObj, }; } @@ -482,7 +510,7 @@ export class MissionManager { // For unmapped IDs, convert NARRATIVE_XYZ -> narrative_xyz // Keep the "narrative_" prefix but lowercase everything - return narrativeId.toLowerCase().replace("narrative_", ""); + return narrativeId.toLowerCase(); } // --- GAMEPLAY LOGIC (Objectives) --- @@ -515,6 +543,8 @@ export class MissionManager { console.log( `[MissionManager] ENEMY_DEATH event received, checking ELIMINATE_ALL objective` ); + // Mark that we had enemies since we received an ENEMY_DEATH event + this._hadEnemies = true; statusChanged = this.checkEliminateAllObjective() || statusChanged; console.log( `[MissionManager] ELIMINATE_ALL check returned statusChanged: ${statusChanged}` @@ -831,6 +861,22 @@ export class MissionManager { // Mark mission as completed this.completedMissions.add(this.activeMissionId); + + // Save mission details for procedural missions (they get removed from registry on refresh) + // This ensures we can display them in the completed missions tab + if ( + this.currentMissionDef.type === "SIDE_QUEST" && + this.activeMissionId?.startsWith("SIDE_OP_") + ) { + // Deep clone the mission definition to preserve it + const missionClone = JSON.parse(JSON.stringify(this.currentMissionDef)); + this.completedMissionDetails.set(this.activeMissionId, missionClone); + console.log( + "MissionManager: Saved procedural mission details for:", + this.activeMissionId + ); + } + console.log( "MissionManager: Mission completed. Active mission ID:", this.activeMissionId diff --git a/src/managers/types.d.ts b/src/managers/types.d.ts index 01cafd5..6d76e8b 100644 --- a/src/managers/types.d.ts +++ b/src/managers/types.d.ts @@ -49,6 +49,7 @@ export interface Objective { */ export interface MissionSaveData { completedMissions: string[]; + completedMissionDetails?: Record; } /** diff --git a/src/systems/TurnSystem.js b/src/systems/TurnSystem.js index f60cec5..ab13970 100644 --- a/src/systems/TurnSystem.js +++ b/src/systems/TurnSystem.js @@ -173,10 +173,18 @@ export class TurnSystem extends EventTarget { // C. Environmental Hazard Processing (Integration Point 3) // Process hazards BEFORE status effects (hazards are environmental, status effects are on the unit) - this.processEnvironmentalHazards(unit); + let unitDied = this.processEnvironmentalHazards(unit); + + // Check if unit died from environmental hazards - if so, handle death and skip turn + if (unitDied && this.onUnitDeathCallback) { + this.onUnitDeathCallback(unit); + // Skip turn for dead unit + this.endTurn(unit); + return; + } // D. Status Effect Tick (The "Upkeep" Step) - const unitDied = this.processStatusEffects(unit); + unitDied = this.processStatusEffects(unit); // Check if unit died from status effects - if so, handle death and skip turn if (unitDied && this.onUnitDeathCallback) { @@ -206,16 +214,17 @@ export class TurnSystem extends EventTarget { * Processes environmental hazards at the unit's position. * Integration Point 3: Environmental Hazard * @param {Unit} unit - The unit to check hazards for + * @returns {boolean} True if the unit died from environmental hazards * @private */ processEnvironmentalHazards(unit) { if (!this.voxelGrid || !this.effectProcessor || !unit.position) { - return; + return false; } const hazard = this.voxelGrid.getHazardAt(unit.position); if (!hazard) { - return; + return false; } // Map hazard IDs to effect definitions @@ -248,6 +257,7 @@ export class TurnSystem extends EventTarget { // Process hazard damage through EffectProcessor // Source is null (environmental), target is the unit + const healthBefore = unit.currentHealth; const result = this.effectProcessor.process(effectDef, null, unit); if (result.success && result.data && result.data.type === "DAMAGE") { console.log( @@ -257,6 +267,9 @@ export class TurnSystem extends EventTarget { ); } + // Check if unit died from hazard damage + const unitDied = healthBefore > 0 && unit.currentHealth <= 0; + // Decrement hazard duration hazard.duration -= 1; if (hazard.duration <= 0) { @@ -264,6 +277,8 @@ export class TurnSystem extends EventTarget { const key = `${unit.position.x},${unit.position.y},${unit.position.z}`; this.voxelGrid.hazardMap.delete(key); } + + return unitDied; } /** diff --git a/src/ui/components/mission-board.js b/src/ui/components/mission-board.js index c5ea6ff..01d7ee9 100644 --- a/src/ui/components/mission-board.js +++ b/src/ui/components/mission-board.js @@ -250,6 +250,7 @@ export class MissionBoard extends LitElement { connectedCallback() { super.connectedCallback(); // Load missions and refresh procedural missions only on first open + // Ensure we wait for GameStateManager to be ready (important after page refresh) this._initialLoad(); // Listen for campaign data changes to refresh completed missions @@ -272,6 +273,9 @@ export class MissionBoard extends LitElement { try { // Ensure missions are loaded before accessing registry await gameStateManager.missionManager._ensureMissionsLoaded(); + // Wait a bit to ensure campaign data has been loaded from persistence + // This is important after page refresh when persistence loads campaign data asynchronously + await new Promise(resolve => setTimeout(resolve, 50)); // Refresh procedural missions if unlocked (to ensure board is populated on first open) // This only happens once when the board is first opened if (gameStateManager.missionManager.areProceduralMissionsUnlocked()) { @@ -297,6 +301,8 @@ export class MissionBoard extends LitElement { _handleCampaignChange() { // Reload missions when campaign data changes (mission completed) + // Force reload of completed missions from MissionManager + this.completedMissions = gameStateManager.missionManager.completedMissions || new Set(); this._loadMissions(); } @@ -331,7 +337,25 @@ export class MissionBoard extends LitElement { // Get all registered missions from MissionManager const missionRegistry = gameStateManager.missionManager.missionRegistry; this.missions = Array.from(missionRegistry.values()); - this.completedMissions = gameStateManager.missionManager.completedMissions || new Set(); + + // Also include completed mission details (for procedural missions that were removed from registry) + const completedMissionDetails = gameStateManager.missionManager.completedMissionDetails || new Map(); + for (const [missionId, missionDef] of completedMissionDetails.entries()) { + // Only add if not already in registry (to avoid duplicates) + if (!this.missions.find(m => m.id === missionId)) { + this.missions.push(missionDef); + } + } + + // Always refresh completed missions from MissionManager to ensure we have the latest data + // This is important after page refresh when persistence has loaded the data + const managerCompletedMissions = gameStateManager.missionManager.completedMissions; + if (managerCompletedMissions instanceof Set) { + this.completedMissions = new Set(managerCompletedMissions); + } else { + this.completedMissions = new Set(managerCompletedMissions || []); + } + console.log('MissionBoard: Loaded completed missions:', Array.from(this.completedMissions)); this.requestUpdate(); } @@ -452,20 +476,71 @@ export class MissionBoard extends LitElement { } _getCompletedMissions() { - return this.missions.filter(mission => { + // Get completed missions that are in the current registry + const completedInRegistry = this.missions.filter(mission => { if (!this._shouldShowMission(mission)) { return false; } return this._isMissionCompleted(mission.id); }); + + // Also include completed missions that are no longer in the registry + // This is important for procedural missions (side quests) that get refreshed + const completedNotInRegistry = []; + for (const missionId of this.completedMissions) { + // Check if this mission is not in the current registry + const missionInRegistry = this.missions.find(m => m.id === missionId); + if (!missionInRegistry) { + // Create a stub mission object for display purposes + // This handles procedural missions that were removed during refresh + const stubMission = this._createStubMission(missionId); + if (stubMission) { + completedNotInRegistry.push(stubMission); + } + } + } + + return [...completedInRegistry, ...completedNotInRegistry]; + } + + _createStubMission(missionId) { + // First, check if we have saved mission details from persistence + const completedMissionDetails = gameStateManager.missionManager.completedMissionDetails || new Map(); + const savedMission = completedMissionDetails.get(missionId); + if (savedMission) { + return savedMission; + } + + // Fallback: Create a minimal mission object for completed missions that are no longer in the registry + // This is primarily for procedural side quests that get refreshed + if (missionId.startsWith('SIDE_OP_')) { + // It's a procedural side quest - create a stub + return { + id: missionId, + type: 'SIDE_QUEST', + config: { + title: missionId.replace('SIDE_OP_', '').replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + description: 'Completed procedural mission (no longer available)', + }, + rewards: { + guaranteed: { + xp: 0, + currency: { aether_shards: 0, ancient_cores: 0 }, + }, + }, + }; + } + // For non-procedural missions, try to load from static mission files if possible + // For now, return null - we could enhance this later to load from mission files + return null; } _getCompletedMissionsByType() { const completed = this._getCompletedMissions(); return { - story: completed.filter(m => m.type === 'STORY'), + story: completed.filter(m => m.type === 'STORY' || m.type === 'TUTORIAL'), sideQuest: completed.filter(m => m.type === 'SIDE_QUEST'), - other: completed.filter(m => m.type !== 'STORY' && m.type !== 'SIDE_QUEST'), + other: completed.filter(m => m.type !== 'STORY' && m.type !== 'SIDE_QUEST' && m.type !== 'TUTORIAL'), }; } diff --git a/test/core/GameLoop/helpers.js b/test/core/GameLoop/helpers.js index 8d339db..feda44b 100644 --- a/test/core/GameLoop/helpers.js +++ b/test/core/GameLoop/helpers.js @@ -77,6 +77,8 @@ export function createMockMissionManager(enemySpawns = []) { return { getActiveMission: sinon.stub().returns(mockMissionDef), + setGridContext: sinon.stub(), + populateZoneCoordinates: sinon.stub(), }; } diff --git a/test/managers/MissionManager.test.js b/test/managers/MissionManager.test.js index 3643cf9..eac4ac5 100644 --- a/test/managers/MissionManager.test.js +++ b/test/managers/MissionManager.test.js @@ -128,9 +128,9 @@ describe("Manager: MissionManager", () => { }, ]; - manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_GOBLIN" }); - manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_OTHER" }); // Should not count - manager.onGameEvent("ENEMY_DEATH", { unitId: "ENEMY_GOBLIN" }); + 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; diff --git a/test/systems/MissionGenerator.test.js b/test/systems/MissionGenerator.test.js index 5102ecd..82327d8 100644 --- a/test/systems/MissionGenerator.test.js +++ b/test/systems/MissionGenerator.test.js @@ -221,7 +221,10 @@ describe("Systems: MissionGenerator", function () { if (mission.objectives.primary[0].type === "REACH_ZONE") { foundRecon = true; expect(mission.objectives.primary[0].target_count).to.equal(3); - expect(mission.objectives.failure_conditions).to.deep.include({ type: "TURN_LIMIT_EXCEEDED" }); + const hasTurnLimit = mission.objectives.failure_conditions.some( + (fc) => fc.type === "TURN_LIMIT_EXCEEDED" + ); + expect(hasTurnLimit).to.be.true; } } expect(foundRecon).to.be.true; @@ -355,12 +358,26 @@ describe("Systems: MissionGenerator", function () { const unlockedRegions = ["BIOME_RUSTING_WASTES"]; it("CoA 19: Should calculate currency with tier multiplier and random factor", () => { - const mission = MissionGenerator.generateSideOp(3, unlockedRegions, []); - - // Base 50 * 2.5 (tier 3) * random(0.8, 1.2) = 100-150 range - const currency = mission.rewards.guaranteed.currency.aether_shards; - expect(currency).to.be.at.least(100); // 50 * 2.5 * 0.8 = 100 - expect(currency).to.be.at.most(150); // 50 * 2.5 * 1.2 = 150 + // Generate multiple missions to account for different archetypes + // Non-assassination missions: Base 50 * 2.5 (tier 3) * random(0.8, 1.2) = 100-150 range + // Assassination missions get 1.5x bonus: 150-225 range + let foundNonAssassination = false; + for (let i = 0; i < 20 && !foundNonAssassination; i++) { + const mission = MissionGenerator.generateSideOp(3, unlockedRegions, []); + const currency = mission.rewards.guaranteed.currency.aether_shards; + const isAssassination = mission.objectives.primary[0].type === "ELIMINATE_UNIT"; + + if (!isAssassination) { + foundNonAssassination = true; + expect(currency).to.be.at.least(100); // 50 * 2.5 * 0.8 = 100 + expect(currency).to.be.at.most(150); // 50 * 2.5 * 1.2 = 150 + } else { + // Assassination missions get 1.5x bonus + expect(currency).to.be.at.least(150); // 50 * 2.5 * 0.8 * 1.5 = 150 + expect(currency).to.be.at.most(225); // 50 * 2.5 * 1.2 * 1.5 = 225 + } + } + expect(foundNonAssassination).to.be.true; }); it("CoA 20: Should give bonus currency for Assassination missions", () => { diff --git a/test/ui/barracks-screen.test.js b/test/ui/barracks-screen.test.js index e05f986..c6b65f0 100644 --- a/test/ui/barracks-screen.test.js +++ b/test/ui/barracks-screen.test.js @@ -128,109 +128,6 @@ describe("UI: BarracksScreen", () => { // Wait for element to be defined and connected await element.updateComplete; - - // Create mock hub stash - mockHubStash = { - currency: { - aetherShards: 1000, - ancientCores: 0, - }, - }; - - // Create mock persistence - mockPersistence = { - loadRun: sinon.stub().resolves({ - inventory: { - runStash: { - currency: { - aetherShards: 500, - ancientCores: 0, - }, - }, - }, - }), - saveRoster: sinon.stub().resolves(), - saveHubStash: sinon.stub().resolves(), - }; - - // Create mock class registry - const mockClassRegistry = new Map(); - mockClassRegistry.set("CLASS_VANGUARD", vanguardDef); - - // Create mock game loop with class registry - mockGameLoop = { - classRegistry: mockClassRegistry, - }; - - // Create mock roster with test units - const testRoster = [ - { - id: "UNIT_1", - name: "Valerius", - classId: "CLASS_VANGUARD", - activeClassId: "CLASS_VANGUARD", - status: "READY", - classMastery: { - CLASS_VANGUARD: { - level: 3, - xp: 150, - skillPoints: 2, - unlockedNodes: [], - }, - }, - history: { missions: 2, kills: 5 }, - }, - { - id: "UNIT_2", - name: "Aria", - classId: "CLASS_VANGUARD", - activeClassId: "CLASS_VANGUARD", - status: "INJURED", - currentHealth: 60, // Injured unit with stored HP - classMastery: { - CLASS_VANGUARD: { - level: 2, - xp: 80, - skillPoints: 1, - unlockedNodes: [], - }, - }, - history: { missions: 1, kills: 2 }, - }, - { - id: "UNIT_3", - name: "Kael", - classId: "CLASS_VANGUARD", - activeClassId: "CLASS_VANGUARD", - status: "READY", - classMastery: { - CLASS_VANGUARD: { - level: 5, - xp: 300, - skillPoints: 3, - unlockedNodes: [], - }, - }, - history: { missions: 5, kills: 12 }, - }, - ]; - - // Create mock roster manager - mockRosterManager = { - roster: testRoster, - rosterLimit: 12, - getDeployableUnits: sinon.stub().returns(testRoster.filter((u) => u.status === "READY")), - save: sinon.stub().returns({ - roster: testRoster, - graveyard: [], - }), - }; - - // Replace gameStateManager properties with mocks - gameStateManager.persistence = mockPersistence; - gameStateManager.rosterManager = mockRosterManager; - gameStateManager.hubStash = mockHubStash; - gameStateManager.gameLoop = mockGameLoop; }); afterEach(() => { diff --git a/test/ui/mission-board.test.js b/test/ui/mission-board.test.js index 6198e38..4c8ae67 100644 --- a/test/ui/mission-board.test.js +++ b/test/ui/mission-board.test.js @@ -9,19 +9,21 @@ describe("UI: MissionBoard", () => { let mockMissionManager; beforeEach(() => { - container = document.createElement("div"); - document.body.appendChild(container); - element = document.createElement("mission-board"); - container.appendChild(element); - - // Mock MissionManager + // Mock MissionManager - set up BEFORE creating element mockMissionManager = { missionRegistry: new Map(), completedMissions: new Set(), _ensureMissionsLoaded: sinon.stub().resolves(), // Mock lazy loading + areProceduralMissionsUnlocked: sinon.stub().returns(false), + refreshProceduralMissions: sinon.stub(), }; gameStateManager.missionManager = mockMissionManager; + + container = document.createElement("div"); + document.body.appendChild(container); + element = document.createElement("mission-board"); + container.appendChild(element); }); afterEach(() => { @@ -89,6 +91,10 @@ describe("UI: MissionBoard", () => { mockMissionManager.missionRegistry.set(mission1.id, mission1); mockMissionManager.missionRegistry.set(mission2.id, mission2); + // Wait for initial load to complete, then trigger a reload + await new Promise(resolve => setTimeout(resolve, 100)); + // Dispatch event to trigger reload + window.dispatchEvent(new CustomEvent('missions-updated')); await waitForUpdate(); const missionCards = queryShadowAll(".mission-card"); @@ -267,9 +273,19 @@ describe("UI: MissionBoard", () => { }; mockMissionManager.missionRegistry.set(mission.id, mission); mockMissionManager.completedMissions.add(mission.id); + await new Promise(resolve => setTimeout(resolve, 100)); + window.dispatchEvent(new CustomEvent('missions-updated')); await waitForUpdate(); + // Switch to completed tab + const completedTab = queryShadow('.tab-button:last-child'); + if (completedTab) { + completedTab.click(); + await waitForUpdate(); + } + const missionCard = queryShadow(".mission-card"); + expect(missionCard).to.exist; expect(missionCard.classList.contains("completed")).to.be.true; expect(missionCard.textContent).to.include("Completed"); }); @@ -283,9 +299,19 @@ describe("UI: MissionBoard", () => { }; mockMissionManager.missionRegistry.set(mission.id, mission); mockMissionManager.completedMissions.add(mission.id); + await new Promise(resolve => setTimeout(resolve, 100)); + window.dispatchEvent(new CustomEvent('missions-updated')); await waitForUpdate(); + // Switch to completed tab + const completedTab = queryShadow('.tab-button:last-child'); + if (completedTab) { + completedTab.click(); + await waitForUpdate(); + } + const missionCard = queryShadow(".mission-card"); + expect(missionCard).to.exist; const selectButton = missionCard.querySelector(".select-button"); expect(selectButton).to.be.null; }); @@ -600,15 +626,29 @@ describe("UI: MissionBoard", () => { mockMissionManager.missionRegistry.set(mission1.id, mission1); mockMissionManager.missionRegistry.set(mission2.id, mission2); mockMissionManager.completedMissions.add("MISSION_01"); + await new Promise(resolve => setTimeout(resolve, 100)); + window.dispatchEvent(new CustomEvent('missions-updated')); + await waitForUpdate(); + // Wait a bit more for the component to process the update + await new Promise(resolve => setTimeout(resolve, 50)); await waitForUpdate(); - const missionCards = queryShadowAll(".mission-card"); - expect(missionCards.length).to.equal(2); - const titles = Array.from(missionCards).map((card) => - card.querySelector(".mission-title")?.textContent.trim() - ); - expect(titles).to.include("First Story"); - expect(titles).to.include("Second Story"); + // Check active tab - should show MISSION_02 (prerequisites met) + const activeCards = queryShadowAll(".missions-grid .mission-card"); + expect(activeCards.length).to.equal(1); + const activeTitle = activeCards[0].querySelector(".mission-title")?.textContent.trim(); + expect(activeTitle).to.equal("Second Story"); + + // Check completed tab - should show MISSION_01 (completed) + const completedTab = queryShadow('.tab-button:last-child'); + if (completedTab) { + completedTab.click(); + await waitForUpdate(); + const completedCards = queryShadowAll(".missions-grid .mission-card"); + expect(completedCards.length).to.equal(1); + const completedTitle = completedCards[0].querySelector(".mission-title")?.textContent.trim(); + expect(completedTitle).to.equal("First Story"); + } }); it("should respect explicit visibility_when_locked setting", async () => {