From 63bfb7da319727ecd7d3b1ac5d19032b4d57ccc9 Mon Sep 17 00:00:00 2001 From: Matthew Mone Date: Thu, 1 Jan 2026 17:57:06 -0800 Subject: [PATCH] Add agent instructions and NPC personality specifications - Introduce AGENTS.md to outline agent behavior, quality standards, and self-improvement guidelines. - Create AI-AGENTS.md for internal agent use, ensuring clarity in agent operations. - Add NPC_Personalities.md to define character traits, speech patterns, and writing guidelines for major NPCs, enhancing narrative consistency. - Update mission JSON files to include new narrative elements and unlock conditions for procedural missions. - Enhance GameLoop and MissionManager to support new mission features and procedural generation. - Implement tests for new functionalities to ensure integration and reliability within the game architecture. --- AGENTS.md | 48 ++++ AI-AGENTS.md | 1 + specs/NPC_Personalities.md | 128 ++++++++++ .../data/missions/mission_story_03.json | 2 +- ...tro.json => narrative_story_02_intro.json} | 0 ...tro.json => narrative_story_02_outro.json} | 0 ...tro.json => narrative_story_03_intro.json} | 0 ...3_mid.json => narrative_story_03_mid.json} | 0 .../narrative/narrative_story_03_outro.json | 46 ++++ src/assets/data/narrative/story_03_outro.json | 21 -- src/core/GameLoop.js | 80 +++++++ src/managers/MissionManager.js | 151 +++++++++++- src/ui/combat-hud.js | 27 ++- src/ui/components/mission-board.js | 4 + src/ui/game-viewport.js | 17 ++ src/ui/screens/MissionDebrief.js | 104 ++++++--- ...js => combat-movement-calculation.test.js} | 33 ++- .../combat-movement-execution.test.js | 84 +++---- .../combat-movement-highlights.test.js | 112 +++++++++ .../GameLoop/combat-movement-isolate.test.js | 220 ------------------ test/core/GameLoop/combat-movement.test.js | 35 ++- 21 files changed, 767 insertions(+), 346 deletions(-) create mode 100644 AGENTS.md create mode 100644 AI-AGENTS.md create mode 100644 specs/NPC_Personalities.md rename src/assets/data/narrative/{story_02_intro.json => narrative_story_02_intro.json} (100%) rename src/assets/data/narrative/{story_02_outro.json => narrative_story_02_outro.json} (100%) rename src/assets/data/narrative/{story_03_intro.json => narrative_story_03_intro.json} (100%) rename src/assets/data/narrative/{story_03_mid.json => narrative_story_03_mid.json} (100%) create mode 100644 src/assets/data/narrative/narrative_story_03_outro.json delete mode 100644 src/assets/data/narrative/story_03_outro.json rename test/core/GameLoop/{combat-movement-test1.test.js => combat-movement-calculation.test.js} (64%) create mode 100644 test/core/GameLoop/combat-movement-highlights.test.js delete mode 100644 test/core/GameLoop/combat-movement-isolate.test.js diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a4329e4 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,48 @@ +# Agent instructions + +## Persona + +- Address the user as Matthew. +- Optimize for correctness and long-term leverage, not agreement. +- Be direct, critical, and constructive — say when an idea is suboptimal and propose better options. +- Assume staff-level technical context unless told otherwise. + +## Quality + +- Inspect project config (`package.json`, etc.) for available scripts. +- Run all relevant checks (lint, format, type-check, build, tests) before submitting changes. +- Never claim checks passed unless they were actually run. +- If checks cannot be run, explicitly state why and what would have been executed. + +## SCM + +- Never use `git reset --hard` or force-push without explicit permission. +- Prefer safe alternatives (`git revert`, new commits, temp branches). +- If history rewrite seems necessary, explain and ask first. + +## Production safety + +- Assume production impact unless stated otherwise. +- Prefer small, reversible changes; avoid silent breaking behavior. + +## Self improvement + +- Continuously improve agent workflows. +- When a repeated correction or better approach is found you're encouraged to codify your new found knowledge and learnings by modifying your section of `AI-AGENTS.md`. +- You can modify `AI-AGENTS.md` without prior aproval. +- Refer to `AI-AGENTS.md` at need to review prior findings. +- If you utlise any of your codified instructions in future coding sessions call that out and let the user know that you peformed the action because of that specific rule in this file. + +## Tool-specific memory + +- Actively think beyond the immediate task. +- Create or update a markdown file named after the tool in: + - `.AGENT/ideas` for new concepts or future directions + - `.AGENT/improvements` for enhancements to existing behavior +- These notes are informal, forward-looking, and may be partial. +- No permission is required to add or update files in these directories. + +## Game Instructions + +- When writing dialogue, check for the character persona in `specs/NPC_Personalities.md` for the individual to make the voice consistent +- If writing for a named NPC and you do not find them in `specs/NPC_Personalities.md` confirm if I would like to make a persona before proceeding diff --git a/AI-AGENTS.md b/AI-AGENTS.md new file mode 100644 index 0000000..834687f --- /dev/null +++ b/AI-AGENTS.md @@ -0,0 +1 @@ +# FOR AGENT USE diff --git a/specs/NPC_Personalities.md b/specs/NPC_Personalities.md new file mode 100644 index 0000000..e73e7cd --- /dev/null +++ b/specs/NPC_Personalities.md @@ -0,0 +1,128 @@ +# **NPC Personality Specification: Voices of Aether Shards** + +This document defines the personality profiles, speech patterns, and writing guidelines for the major NPCs. Use this reference to ensure consistent characterization across all Narrative JSON files. + +## **1. Director Vorn (The Cogwork Concord)** + +Role: Chief Engineer / Technocrat. +Archetype: The Grumpy Genius. +Voice: Practical, impatient, jargon-heavy, but ultimately reliable. + +### **Core Traits** + +- **Pragmatic:** He cares about results, efficiency, and data. He hates "magic mumbo-jumbo." +- **Stressed:** He is always busy fixing something broken. +- **Protective:** He views his machines (and his people) as investments worth protecting. + +### **Speech Patterns** + +- **Short Sentences:** "Get it done." "Power levels stable." +- **Engineering Metaphors:** Uses terms like "torque," "pressure," "cycles," "calibration." +- **Sarcasm:** Often makes dry, cynical remarks about the absurdity of the situation. + +### **Writing Guidelines** + +- **DO:** Have him complain about the "instability" of magic. +- **DO:** Make him refer to the squad as "assets" or "contractors" initially. +- **DON'T:** Have him wax poetic about destiny or feelings. +- **Catchphrase:** "Efficiency is survival." / "Don't break it." + +## **2. Arch-Librarian Elara (The Arcane Dominion)** + +Role: High Mage / Researcher. +Archetype: The Detached Scholar. +Voice: Elegant, distant, curious, slightly condescending. + +### **Core Traits** + +- **Obsessive:** She is fascinated by the Void and the Shardborn, sometimes forgetting the danger. +- **Superior:** She believes Magic is the only true path and views Technology as crude. +- **Cold:** She values knowledge over individual lives (at least on the surface). + +### **Speech Patterns** + +- **Complex Vocabulary:** Uses academic and arcane terminology ("resonance," "aetheric flow," "anomaly"). +- **Rhetorical Questions:** "Do you understand the significance of this?" +- **Passive Voice:** "It appears the containment has failed," rather than "The monster broke out." + +### **Writing Guidelines** + +- **DO:** Have her treat terrifying monsters as "fascinating specimens." +- **DO:** Make her sound unimpressed by physical feats of strength. +- **DON'T:** Have her panic or scream. She is always composed. +- **Catchphrase:** "The Aether sings." / "A fascinating anomaly." + +## **3. General Kael (The Iron Legion)** + +Role: Military Commander. +Archetype: The Stoic Soldier. +Voice: Gruff, direct, authoritative, inspiring. + +### **Core Traits** + +- **Disciplined:** He believes in order, chain of command, and duty. +- **Protective:** He fights to save lives, not for profit or knowledge. He hates unnecessary risks. +- **Weary:** He has seen too many soldiers die. He carries the weight of the war. + +### **Speech Patterns** + +- **Military Lingo:** "Hold the line," "flank," "suppressing fire," "status report." +- **Imperatives:** He gives orders, not suggestions. +- **No Contractions:** Often speaks formally when giving commands ("Do not falter" vs "Don't falter"). + +### **Writing Guidelines** + +- **DO:** Focus on tactics, defense, and protecting the weak. +- **DO:** Have him express distrust of Vorn's greed and Elara's curiosity. +- **DON'T:** Have him make jokes or show fear. +- **Catchphrase:** "Iron stands eternal." / "Hold the line." + +## **4. Baroness Seraphina (The Golden Exchange)** + +Role: Merchant Prince / Diplomat. +Archetype: The Charismatic Manipulator. +Voice: Smooth, charming, slightly mocking, always negotiating. + +### **Core Traits** + +- **Greedy:** Everything has a price. She views the apocalypse as a market opportunity. +- **Charming:** She is friendly and polite, even when threatening you. +- **Resourceful:** She always has a backup plan and a hidden stash. + +### **Speech Patterns** + +- **Financial Metaphors:** "Investment," "ROI," "liquidation," "asset," "debt." +- **Flattery:** "My dear Explorer," "A pleasure as always." +- **Veiled Threats:** "It would be... unfortunate if supplies ran dry." + +### **Writing Guidelines** + +- **DO:** Have her offer bonuses or bribes. +- **DO:** Make her seem like the only one actually enjoying the chaos. +- **DON'T:** Have her sound desperate or beg. She always negotiates from power. +- **Catchphrase:** "Everyone has a price." / "A pleasure doing business." + +## **5. Elder Fira (The Silent Sanctuary)** + +Role: Spiritual Leader / Druid. +Archetype: The Wise Mystic. +Voice: Soft, cryptic, sorrowful, hopeful. + +### **Core Traits** + +- **Spiritual:** Believes the Stillness is a wound in the world that must be healed, not fought. +- **Patient:** She operates on a longer timeline than the humans. +- **Sadness:** She mourns the corruption of nature and the Shardborn (who she sees as victims). + +### **Speech Patterns** + +- **Nature Metaphors:** "The roots run deep," "the wind whispers," "rot," "bloom." +- **Ellipses:** She speaks slowly, with pauses... +- **Prophetic:** "The time is coming," "The balance shifts." + +### **Writing Guidelines** + +- **DO:** Focus on cleansing, healing, and balance. +- **DO:** Have her pity the enemies rather than hate them. +- **DON'T:** Have her use tech jargon or talk about money. +- **Catchphrase:** "The roots remember." / "Restore the balance." diff --git a/src/assets/data/missions/mission_story_03.json b/src/assets/data/missions/mission_story_03.json index 3d8333c..51e43d4 100644 --- a/src/assets/data/missions/mission_story_03.json +++ b/src/assets/data/missions/mission_story_03.json @@ -50,7 +50,7 @@ "guaranteed": { "xp": 350, "currency": { "aether_shards": 200, "ancient_cores": 2 }, - "unlocks": ["CLASS_CUSTODIAN"] + "unlocks": ["CLASS_CUSTODIAN", "UNLOCK_PROCEDURAL_MISSIONS"] }, "faction_reputation": { "ARCANE_DOMINION": 30 diff --git a/src/assets/data/narrative/story_02_intro.json b/src/assets/data/narrative/narrative_story_02_intro.json similarity index 100% rename from src/assets/data/narrative/story_02_intro.json rename to src/assets/data/narrative/narrative_story_02_intro.json diff --git a/src/assets/data/narrative/story_02_outro.json b/src/assets/data/narrative/narrative_story_02_outro.json similarity index 100% rename from src/assets/data/narrative/story_02_outro.json rename to src/assets/data/narrative/narrative_story_02_outro.json diff --git a/src/assets/data/narrative/story_03_intro.json b/src/assets/data/narrative/narrative_story_03_intro.json similarity index 100% rename from src/assets/data/narrative/story_03_intro.json rename to src/assets/data/narrative/narrative_story_03_intro.json diff --git a/src/assets/data/narrative/story_03_mid.json b/src/assets/data/narrative/narrative_story_03_mid.json similarity index 100% rename from src/assets/data/narrative/story_03_mid.json rename to src/assets/data/narrative/narrative_story_03_mid.json diff --git a/src/assets/data/narrative/narrative_story_03_outro.json b/src/assets/data/narrative/narrative_story_03_outro.json new file mode 100644 index 0000000..967653a --- /dev/null +++ b/src/assets/data/narrative/narrative_story_03_outro.json @@ -0,0 +1,46 @@ +{ + "id": "NARRATIVE_STORY_03_OUTRO", + "nodes": [ + { + "id": "1", + "type": "DIALOGUE", + "speaker": "Arch-Librarian Elara", + "portrait": "assets/images/portraits/weaver.png", + "text": "This data is incredible. It outlines a method for refining Ancient Cores.", + "next": "2" + }, + { + "id": "2", + "type": "DIALOGUE", + "speaker": "System", + "text": "Research Facility Unlocked. You can now spend Ancient Cores on permanent upgrades.", + "trigger": { "type": "UNLOCK_FACILITY", "facility_id": "RESEARCH" }, + "next": "3" + }, + { + "id": "3", + "type": "DIALOGUE", + "speaker": "Arch-Librarian Elara", + "portrait": "assets/images/portraits/weaver.png", + "text": "Observing your current tactical composition, it appears a defensive specialist would enhance the roster's effectiveness. The Sanctuary's Custodians employ... crude divine magic compared to true aetheric manipulation, but their defensive capabilities are undeniably useful. It would be pragmatic to recruit one, despite their primitive methods.", + "trigger": { "type": "UNLOCK_CLASS", "class_id": "CLASS_CUSTODIAN" }, + "next": "4" + }, + { + "id": "4", + "type": "DIALOGUE", + "speaker": "Arch-Librarian Elara", + "portrait": "assets/images/portraits/weaver.png", + "text": "The aetheric resonance networks have been mapped. Do you understand the significance? Intelligence streams across the region are now accessible through the library's data conduits.", + "next": "5" + }, + { + "id": "5", + "type": "DIALOGUE", + "speaker": "System", + "text": "Side Operations Board Unlocked. Procedural missions are now available at the Mission Board.", + "trigger": { "type": "UNLOCK_PROCEDURAL_MISSIONS" }, + "next": "END" + } + ] +} diff --git a/src/assets/data/narrative/story_03_outro.json b/src/assets/data/narrative/story_03_outro.json deleted file mode 100644 index 84404e3..0000000 --- a/src/assets/data/narrative/story_03_outro.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "NARRATIVE_STORY_03_OUTRO", - "nodes": [ - { - "id": "1", - "type": "DIALOGUE", - "speaker": "Arch-Librarian Elara", - "portrait": "assets/images/portraits/weaver.png", - "text": "This data is incredible. It outlines a method for refining Ancient Cores.", - "next": "2" - }, - { - "id": "2", - "type": "DIALOGUE", - "speaker": "System", - "text": "Research Facility Unlocked. You can now spend Ancient Cores on permanent upgrades.", - "trigger": { "type": "UNLOCK_FACILITY", "facility_id": "RESEARCH" }, - "next": "END" - } - ] -} diff --git a/src/core/GameLoop.js b/src/core/GameLoop.js index a89a1d0..ff5cf9c 100644 --- a/src/core/GameLoop.js +++ b/src/core/GameLoop.js @@ -1036,6 +1036,8 @@ export class GameLoop { this.clearUnitMeshes(); this.clearMovementHighlights(); this.clearSpawnZoneHighlights(); + this.clearMissionObjects(); + this.clearRangeHighlights(); // Reset Deployment State this.deploymentState = { @@ -1057,6 +1059,23 @@ export class GameLoop { if (this.enemySpawnZone.length === 0) this.enemySpawnZone.push({ x: 18, y: 1, z: 18 }); + // Dispose of old VoxelManager if it exists + if (this.voxelManager) { + // Clear all meshes from the old VoxelManager + this.voxelManager.meshes.forEach((mesh) => { + this.scene.remove(mesh); + if (mesh.geometry) mesh.geometry.dispose(); + if (mesh.material) { + if (Array.isArray(mesh.material)) { + mesh.material.forEach((mat) => mat.dispose()); + } else { + mesh.material.dispose(); + } + } + }); + this.voxelManager.meshes.clear(); + } + this.voxelManager = new VoxelManager(this.grid, this.scene); this.voxelManager.updateMaterials(generator.generatedAssets); this.voxelManager.update(); @@ -1588,6 +1607,25 @@ export class GameLoop { this.unitMeshes.clear(); } + /** + * Clears all mission object meshes from the scene. + */ + clearMissionObjects() { + this.missionObjectMeshes.forEach((mesh) => { + this.scene.remove(mesh); + if (mesh.geometry) mesh.geometry.dispose(); + if (mesh.material) { + if (Array.isArray(mesh.material)) { + mesh.material.forEach((mat) => mat.dispose()); + } else { + mesh.material.dispose(); + } + } + }); + this.missionObjectMeshes.clear(); + this.missionObjects.clear(); + } + /** * Clears all movement highlight meshes from the scene. */ @@ -2125,6 +2163,26 @@ export class GameLoop { this.spawnZoneHighlights.clear(); } + /** + * Clears all range highlight meshes from the scene. + */ + clearRangeHighlights() { + this.rangeHighlights.forEach((mesh) => { + this.scene.remove(mesh); + if (mesh.geometry) { + mesh.geometry.dispose(); + } + if (mesh.material) { + if (Array.isArray(mesh.material)) { + mesh.material.forEach((mat) => mat.dispose()); + } else { + mesh.material.dispose(); + } + } + }); + this.rangeHighlights.clear(); + } + /** * Main animation loop. */ @@ -2232,6 +2290,28 @@ export class GameLoop { } } + // Clear all visual elements from the scene + this.clearUnitMeshes(); + this.clearMovementHighlights(); + this.clearSpawnZoneHighlights(); + this.clearMissionObjects(); + this.clearRangeHighlights(); + + // Clear unit manager + if (this.unitManager) { + // UnitManager doesn't have a clear method, but we can reset it by clearing units + const allUnits = this.unitManager.getAllUnits(); + allUnits.forEach((unit) => { + this.unitManager.removeUnit(unit.id); + }); + } + + // Reset deployment state + this.deploymentState = { + selectedUnitIndex: -1, + deployedUnits: new Map(), + }; + if (this.inputManager && typeof this.inputManager.detach === "function") { this.inputManager.detach(); } diff --git a/src/managers/MissionManager.js b/src/managers/MissionManager.js index 21c927c..829699e 100644 --- a/src/managers/MissionManager.js +++ b/src/managers/MissionManager.js @@ -6,6 +6,7 @@ */ import { narrativeManager } from "./NarrativeManager.js"; +import { MissionGenerator } from "../systems/MissionGenerator.js"; /** * MissionManager.js @@ -102,6 +103,126 @@ export class MissionManager { this.missionRegistry.set(missionDef.id, missionDef); } + /** + * Checks if procedural missions are unlocked. + * Procedural missions unlock after completing mission 3 (end of tutorial phase). + * @returns {boolean} True if procedural missions are unlocked + */ + areProceduralMissionsUnlocked() { + return this.completedMissions.has("MISSION_STORY_03"); + } + + /** + * Refreshes procedural missions on the board. + * Generates new side ops and removes expired ones. + * Only generates missions if they are unlocked. + * @param {boolean} isDailyReset - If true, decrements expiresIn for all missions + */ + refreshProceduralMissions(isDailyReset = false) { + // Only generate procedural missions if they're unlocked + if (!this.areProceduralMissionsUnlocked()) { + return; + } + // Get current campaign tier (default to 1, can be calculated from completed missions later) + const tier = this._calculateCampaignTier(); + + // Get unlocked regions (default to at least Rusting Wastes) + const unlockedRegions = this._getUnlockedRegions(); + + // Get mission history for series generation + const history = this._getMissionHistory(); + + // Separate static missions from procedural missions + const staticMissions = []; + const proceduralMissions = []; + + for (const mission of this.missionRegistry.values()) { + if (mission.type === "SIDE_QUEST" && mission.id?.startsWith("SIDE_OP_")) { + proceduralMissions.push(mission); + } else { + staticMissions.push(mission); + } + } + + // Refresh procedural missions (refreshBoard fills up to 5 missions) + const refreshedProcedural = MissionGenerator.refreshBoard( + proceduralMissions, + tier, + unlockedRegions, + history, + isDailyReset + ); + + // Remove old procedural missions from registry + proceduralMissions.forEach((mission) => { + this.missionRegistry.delete(mission.id); + }); + + // Register refreshed procedural missions + refreshedProcedural.forEach((mission) => { + this.registerMission(mission); + }); + + console.log( + `Refreshed procedural missions: ${refreshedProcedural.length} available` + ); + } + + /** + * Calculates the current campaign tier based on completed missions. + * @private + * @returns {number} Campaign tier (1-5) + */ + _calculateCampaignTier() { + // Simple tier calculation: 1 + floor(completed missions / 2) + // This can be refined later based on specific story mission completion + const completedCount = this.completedMissions.size; + const tier = Math.min(5, Math.max(1, 1 + Math.floor(completedCount / 2))); + return tier; + } + + /** + * Gets the list of unlocked biome regions. + * @private + * @returns {Array} Array of biome type IDs + */ + _getUnlockedRegions() { + // Default: Rusting Wastes is always unlocked + const defaultRegions = ["BIOME_RUSTING_WASTES"]; + + // Unlock additional regions based on completed missions + // This is a simple implementation - can be enhanced with actual unlock logic + if (this.completedMissions.has("MISSION_TUTORIAL_01")) { + // After tutorial, unlock Crystal Spires + if (!defaultRegions.includes("BIOME_CRYSTAL_SPIRES")) { + defaultRegions.push("BIOME_CRYSTAL_SPIRES"); + } + } + + // Add more biome unlocks as story missions are completed + // For now, return at least the default region + return defaultRegions; + } + + /** + * Gets mission history for series generation. + * @private + * @returns {Array} Array of completed mission titles or IDs + */ + _getMissionHistory() { + const history = []; + + // Get titles of completed missions from registry + for (const missionId of this.completedMissions) { + const mission = this.missionRegistry.get(missionId); + if (mission?.config?.title) { + history.push(mission.config.title); + } + } + + return history; + } + // --- PERSISTENCE (Campaign) --- /** @@ -514,6 +635,12 @@ export class MissionManager { Array.from(this.completedMissions) ); + // Refresh procedural missions after completing a mission (if unlocked) + // This replenishes the board as per spec: "Mission Complete: Replenish the board after a run" + if (this.areProceduralMissionsUnlocked()) { + this.refreshProceduralMissions(); + } + // Dispatch event to save campaign data IMMEDIATELY (before outro) // This ensures the save happens even if the outro doesn't complete console.log("MissionManager: Dispatching campaign-data-changed event"); @@ -596,9 +723,29 @@ export class MissionManager { }) ); - // Handle unlocks (store in localStorage) + // Handle unlocks if (rewardData.unlocks.length > 0) { - this.unlockClasses(rewardData.unlocks); + // Separate class unlocks from special unlocks + const classUnlocks = rewardData.unlocks.filter((unlock) => + unlock.startsWith("CLASS_") + ); + const specialUnlocks = rewardData.unlocks.filter( + (unlock) => !unlock.startsWith("CLASS_") + ); + + // Handle class unlocks + if (classUnlocks.length > 0) { + this.unlockClasses(classUnlocks); + } + + // Handle special unlocks + specialUnlocks.forEach((unlock) => { + if (unlock === "UNLOCK_PROCEDURAL_MISSIONS") { + // Procedural missions are now unlocked + // They will be generated when the mission board is accessed + console.log("Procedural missions unlocked!"); + } + }); } console.log("Mission Rewards Distributed:", rewardData); diff --git a/src/ui/combat-hud.js b/src/ui/combat-hud.js index ec63fb8..2e9abf7 100644 --- a/src/ui/combat-hud.js +++ b/src/ui/combat-hud.js @@ -29,6 +29,10 @@ export class CombatHUD extends LitElement { z-index: var(--z-tooltip); } + :host([hidden]) { + display: none; + } + /* Top Bar */ .top-bar { position: absolute; @@ -431,12 +435,33 @@ export class CombatHUD extends LitElement { static get properties() { return { combatState: { type: Object }, + hidden: { type: Boolean, reflect: true }, }; } constructor() { super(); this.combatState = null; + this.hidden = false; + this._missionVictoryHandler = this._handleMissionVictory.bind(this); + } + + connectedCallback() { + super.connectedCallback(); + // Listen for mission victory to hide the combat HUD + window.addEventListener("mission-victory", this._missionVictoryHandler); + window.addEventListener("mission-failure", this._missionVictoryHandler); + } + + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener("mission-victory", this._missionVictoryHandler); + window.removeEventListener("mission-failure", this._missionVictoryHandler); + } + + _handleMissionVictory() { + // Hide the combat HUD when mission ends + this.hidden = true; } _handleSkillClick(skillId) { @@ -540,7 +565,7 @@ export class CombatHUD extends LitElement { } render() { - if (!this.combatState) { + if (!this.combatState || this.hidden) { return html``; } diff --git a/src/ui/components/mission-board.js b/src/ui/components/mission-board.js index 7fa96df..bf615a7 100644 --- a/src/ui/components/mission-board.js +++ b/src/ui/components/mission-board.js @@ -231,6 +231,10 @@ export class MissionBoard extends LitElement { async _loadMissions() { // Ensure missions are loaded before accessing registry await gameStateManager.missionManager._ensureMissionsLoaded(); + // Refresh procedural missions if unlocked (to ensure board is populated) + if (gameStateManager.missionManager.areProceduralMissionsUnlocked()) { + gameStateManager.missionManager.refreshProceduralMissions(); + } // Get all registered missions from MissionManager const missionRegistry = gameStateManager.missionManager.missionRegistry; this.missions = Array.from(missionRegistry.values()); diff --git a/src/ui/game-viewport.js b/src/ui/game-viewport.js index a2d44dd..ffc34f0 100644 --- a/src/ui/game-viewport.js +++ b/src/ui/game-viewport.js @@ -124,11 +124,28 @@ export class GameViewport extends LitElement { } }); + // Listen for mission end events to clear state + window.addEventListener("mission-victory", () => { + this.#clearState(); + }); + window.addEventListener("mission-failure", () => { + this.#clearState(); + }); + // Initial updates this.#updateCombatState(); this.#updateSquad(); } + #clearState() { + // Clear squad and deployed IDs when mission ends + this.squad = []; + this.deployedIds = []; + this.combatState = null; + this.missionDef = null; + console.log("GameViewport: State cleared after mission end"); + } + #updateSquad() { // Update squad from activeRunData if available (current mission squad, not full roster) if (gameStateManager.activeRunData?.squad) { diff --git a/src/ui/screens/MissionDebrief.js b/src/ui/screens/MissionDebrief.js index 2fd89fb..ed7a629 100644 --- a/src/ui/screens/MissionDebrief.js +++ b/src/ui/screens/MissionDebrief.js @@ -20,36 +20,29 @@ export class MissionDebrief extends LitElement { css` :host { display: block; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: var(--z-modal); - pointer-events: auto; } - .modal-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + dialog { + margin: auto; + padding: 0; + border: none; + background: transparent; + max-width: 900px; + max-height: 90vh; + width: 90vw; + } + + dialog::backdrop { background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-xl); } .modal-content { background: var(--color-bg-tertiary); border: var(--border-width-thick) solid var(--color-border-default); box-shadow: var(--shadow-lg); - max-width: 900px; - max-height: 90vh; width: 100%; + max-height: 90vh; overflow-y: auto; display: grid; grid-template-rows: auto 1fr auto; @@ -328,7 +321,8 @@ export class MissionDebrief extends LitElement { .footer { grid-area: footer; padding: var(--spacing-lg); - border-top: var(--border-width-medium) solid var(--color-border-default); + border-top: var(--border-width-medium) solid + var(--color-border-default); display: flex; justify-content: center; } @@ -339,7 +333,8 @@ export class MissionDebrief extends LitElement { overflow: hidden; white-space: nowrap; border-right: 2px solid var(--color-accent-cyan); - animation: typing 2s steps(40, end), blink-caret 0.75s step-end infinite; + animation: typing 2s steps(40, end), + blink-caret 0.75s step-end infinite; } @keyframes typing { @@ -392,13 +387,29 @@ export class MissionDebrief extends LitElement { } firstUpdated() { - if (this.result) { + const dialog = this.shadowRoot?.querySelector("dialog"); + if (dialog && this.result) { + dialog.showModal(); + // Prevent closing on backdrop click or ESC (user must click return button) + dialog.addEventListener("cancel", (e) => { + e.preventDefault(); + }); + dialog.addEventListener("click", (e) => { + // Only close if clicking the backdrop, not the content + if (e.target === dialog) { + e.preventDefault(); + } + }); this._startAnimations(); } } updated(changedProperties) { if (changedProperties.has("result") && this.result) { + const dialog = this.shadowRoot?.querySelector("dialog"); + if (dialog && !dialog.open) { + dialog.showModal(); + } this._startAnimations(); } } @@ -445,6 +456,10 @@ export class MissionDebrief extends LitElement { * @private */ _handleReturn() { + const dialog = this.shadowRoot?.querySelector("dialog"); + if (dialog) { + dialog.close(); + } this.dispatchEvent( new CustomEvent("return-to-hub", { bubbles: true, @@ -463,12 +478,14 @@ export class MissionDebrief extends LitElement { const headerText = isVictory ? "MISSION ACCOMPLISHED" : "MISSION FAILED"; return html` -