diff --git a/.cursor/rules/RULE.md b/.cursor/rules/RULE.md
new file mode 100644
index 0000000..521ae8b
--- /dev/null
+++ b/.cursor/rules/RULE.md
@@ -0,0 +1,39 @@
+---
+description: High-level technical standards, file structure, and testing requirements for the Aether Shards project.
+globs: src/*.js, test/*.js**
+---
+
+# **General Project Standards**
+
+## **Tech Stack**
+
+- **Engine:** Three.js \+ Vanilla JavaScript (ES Modules).
+- **UI:** LitElement (Web Components).
+- **State:** Custom State Managers (Singletons).
+- **Persistence:** IndexedDB (via Persistence.js).
+- **Testing:** @web/test-runner \+ @esm-bundle/chai \+ sinon.
+
+## **File Structure**
+
+- src/core/: The main loop, state management, and input handling.
+- src/grid/: Voxel data structures and rendering logic.
+- src/units/: Entity classes (Explorer, Enemy).
+- src/managers/: Logic controllers (UnitManager, MissionManager).
+- src/systems/: Gameplay logic (TurnSystem, EffectProcessor, AI).
+- src/generation/: Procedural algorithms.
+- src/ui/: LitElement components.
+- assets/data/: JSON definitions (Classes, Items, Missions).
+
+## **Testing Mandate (TDD)**
+
+1. **Test First:** All logic must have a corresponding test suite in test/.
+2. **Conditions of Acceptance (CoA):** Every feature must define CoAs, and tests must explicitely verify them.
+3. **Headless WebGL:** Tests involving Three.js/WebGL must handle headless contexts (SwiftShader) gracefully or use mocks.
+4. **No Global Side Effects:** Tests must clean up DOM elements and Three.js resources (dispose()) after execution.
+
+## **Coding Style**
+
+- Use ES6 Modules (import/export).
+- Prefer const over let. No var.
+- Use JSDoc for all public methods and complex algorithms.
+- **No Circular Dependencies:** Managers should not import GameLoop. GameLoop acts as the orchestrator.
diff --git a/.cursor/rules/core/CombatIntegration/RULE.md b/.cursor/rules/core/CombatIntegration/RULE.md
new file mode 100644
index 0000000..43929e6
--- /dev/null
+++ b/.cursor/rules/core/CombatIntegration/RULE.md
@@ -0,0 +1,161 @@
+---
+description: Combat integration - wiring TurnSystem and MovementSystem into the GameLoop
+globs: src/core/GameLoop.js, src/core/GameLoop.ts
+alwaysApply: false
+---
+
+# **Combat Integration Rule**
+
+This rule defines how the TurnSystem and MovementSystem integrate into the existing GameLoop.
+
+## **1. System Ownership**
+
+The GameLoop is the central owner of these systems. It ensures they share the same source of truth (VoxelGrid and UnitManager).
+
+**GameLoop Structure Update:**
+
+```js
+class GameLoop {
+ constructor() {
+ // ... existing systems ...
+ this.grid = null;
+ this.unitManager = null;
+
+ // NEW: Combat Logic Systems
+ this.turnSystem = null; // Manages Initiative & Round state
+ this.movementSystem = null; // Manages Pathfinding & Position updates
+ }
+
+ init(container) {
+ // ... existing init ...
+ // Instantiate logic systems (they are stateless until startLevel)
+ this.turnSystem = new TurnSystem();
+ this.movementSystem = new MovementSystem();
+ }
+}
+```
+
+## **2. Integration Point: Level Initialization**
+
+When `startLevel()` runs, the new Grid and UnitManager must be injected into the combat systems so they act on the current map.
+
+**Location:** `src/core/GameLoop.js` -> `startLevel()`
+
+```js
+async startLevel(runData) {
+ // ... generate grid and units ...
+
+ // WIRING: Connect Systems to Data
+ this.movementSystem.setContext(this.grid, this.unitManager);
+ this.turnSystem.setContext(this.unitManager);
+
+ // WIRING: Listen for Turn Changes (to update UI/Input state)
+ this.turnSystem.addEventListener('turn-start', (e) => this._onTurnStart(e.detail));
+}
+```
+
+## **3. Integration Point: Transition to Combat**
+
+The transition from "Placing Units" to "Fighting" happens in `finalizeDeployment`. This is where the Turn System takes control.
+
+**Location:** `src/core/GameLoop.js` -> `finalizeDeployment()`
+
+```js
+finalizeDeployment() {
+ // ... spawn enemies ...
+
+ this.setPhase('COMBAT');
+
+ // WIRING: Hand control to TurnSystem
+ const allUnits = this.unitManager.getAllUnits();
+ this.turnSystem.startCombat(allUnits);
+
+ // UI Update: Show Combat HUD
+ // (Handled via event listeners in index.html)
+}
+```
+
+## **4. Integration Point: Input Routing (The "Game Loop")**
+
+When the game is in COMBAT phase, inputs must be routed to the active system based on context (Moving vs Targeting).
+
+**Location:** `src/core/GameLoop.js` -> `triggerSelection()` (called by InputManager)
+
+```js
+triggerSelection() {
+ const cursor = this.inputManager.getCursorPosition();
+
+ // PHASE: DEPLOYMENT (Existing Logic)
+ if (this.phase === 'DEPLOYMENT') {
+ // ... deploy logic ...
+ }
+
+ // PHASE: COMBAT (New Logic)
+ else if (this.phase === 'COMBAT') {
+ const activeUnit = this.turnSystem.getActiveUnit();
+
+ // Security Check: Is it actually the player's turn?
+ if (activeUnit.team !== 'PLAYER') return;
+
+ // Context A: Unit is trying to MOVE
+ if (this.combatState === 'SELECTING_MOVE') {
+ // DELEGATE to MovementSystem
+ if (this.movementSystem.isValidMove(activeUnit, cursor)) {
+ this.movementSystem.executeMove(activeUnit, cursor);
+ // Updating AP is handled internally or via event
+ }
+ }
+
+ // Context B: Unit is targeting a SKILL
+ else if (this.combatState === 'TARGETING_SKILL') {
+ // Delegate to SkillSystem (Future)
+ }
+ }
+}
+```
+
+## **5. Visualizing Range (The "Update Loop")**
+
+The blue movement grid needs to update whenever the active unit changes or moves.
+
+**Location:** `src/core/GameLoop.js` -> `animate()` or Event Handler
+
+```js
+_onTurnStart(unit) {
+ if (unit.team === 'PLAYER') {
+ // Ask MovementSystem for reachable tiles
+ const tiles = this.movementSystem.getReachableTiles(unit);
+ // Ask VoxelManager to highlight them
+ this.voxelManager.highlightTiles(tiles, 'BLUE');
+ } else {
+ this.voxelManager.clearHighlights();
+ // Trigger AI processing
+ }
+}
+```
+
+## **6. Conditions of Acceptance (CoA)**
+
+**CoA 1: System Initialization**
+
+- When `startLevel()` is called, both `turnSystem` and `movementSystem` must receive valid references to the current grid and unit manager
+
+**CoA 2: Phase Transitions**
+
+- Calling `finalizeDeployment()` must transition the game to COMBAT phase and initialize the turn queue
+
+**CoA 3: Input Routing**
+
+- During COMBAT phase, player inputs must be routed to the appropriate system (MovementSystem for moves, SkillTargetingSystem for skills)
+
+**CoA 4: Visual Feedback**
+
+- When a player unit's turn starts, the movement range must be highlighted on the grid
+- When an enemy unit's turn starts, highlights must be cleared
+
+## **7. Implementation Requirements**
+
+- **Responsibility:** The GameLoop is the "God Object" responsible for tying systems together. It owns the Scene, Renderer, Grid, and Managers
+- **Phases:** The loop must respect the current phase: INIT, DEPLOYMENT, COMBAT, RESOLUTION
+- **Input Routing:** The loop routes raw inputs from InputManager to the appropriate system (e.g., MovementSystem vs SkillTargeting) based on the current Phase
+
diff --git a/.cursor/rules/core/RULE.md b/.cursor/rules/core/RULE.md
new file mode 100644
index 0000000..37155ca
--- /dev/null
+++ b/.cursor/rules/core/RULE.md
@@ -0,0 +1,31 @@
+---
+description: Standards for Three.js integration, VoxelGrid, and the Game Loop.
+globs: src/core/*.js, src/grid/*.js**
+---
+
+# **Game Engine Standards**
+
+## **The Game Loop**
+
+- **Responsibility:** The GameLoop is the "God Object" responsible for tying systems together. It owns the Scene, Renderer, Grid, and Managers.
+- **Phases:** The loop must respect the current phase: INIT, DEPLOYMENT, COMBAT, RESOLUTION.
+- **Input Routing:** The loop routes raw inputs from InputManager to the appropriate system (e.g., MovementSystem vs SkillTargeting) based on the current Phase.
+
+## **Voxel System**
+
+1. **Separation of Concerns:**
+ - VoxelGrid.js: **Pure Data**. Stores IDs in Uint8Array. Handles physics queries (isSolid). No Three.js dependencies.
+ - VoxelManager.js: **Rendering**. Reads VoxelGrid and updates THREE.InstancedMesh. Handles materials and textures.
+2. **Performance:**
+ - Never create individual THREE.Mesh objects for terrain. Use InstancedMesh.
+ - Hide "Air" voxels by scaling them to 0 rather than removing them from the InstanceMatrix (unless refactoring for chunking).
+3. **Coordinates:**
+ - Use {x, y, z} objects for positions.
+ - Y is **Up**.
+ - Grid coordinates are integers.
+
+## **Input**
+
+- Use InputManager for all hardware interaction.
+- Support Mouse, Keyboard, and Gamepad seamlessly.
+- Raycasting should return integer Grid Coordinates.
diff --git a/.cursor/rules/data/Inventory/RULE.md b/.cursor/rules/data/Inventory/RULE.md
new file mode 100644
index 0000000..42f29aa
--- /dev/null
+++ b/.cursor/rules/data/Inventory/RULE.md
@@ -0,0 +1,189 @@
+---
+description: Inventory system architecture - item management for individual Explorer loadouts and shared storage
+globs: src/managers/InventoryManager.js, src/types/Inventory.ts, src/ui/components/InventoryUI.js
+alwaysApply: false
+---
+
+# **Inventory System Rule**
+
+The Inventory system operates in two distinct contexts:
+
+1. **Run Context (The Expedition):**
+ - **Unit Loadout:** Active gear affecting stats. Locked during combat (usually), editable during rest
+ - **Run Stash (The Bag):** Temporary storage for loot found during the run. Infinite (or high) capacity
+ - **Rule:** If the squad wipes, the _Run Stash_ is lost. Only equipped gear might be recovered (depending on difficulty settings)
+2. **Hub Context (The Armory):**
+ - **Master Stash:** Persistent storage for all unequipped items
+ - **Management:** Players move items between the Master Stash and Unit Loadouts to prepare for the next run
+ - **Extraction:** Upon successful run completion, _Run Stash_ contents are merged into _Master Stash_
+
+## **1. Visual Description (UI)**
+
+### **A. Unit Loadout (The Paper Doll)**
+
+- **Visual:** A silhouette or 3D model of the character
+- **Slots:**
+ - **Primary Hand:** Weapon (Sword, Staff, Wrench)
+ - **Off-Hand:** Shield, Focus, Tool, or 2H (occupies both)
+ - **Body:** Armor (Plate, Robes, Vest)
+ - **Accessory/Relic:** Stat boosters or Passive enablers
+ - **Belt (2 Slots):** Consumables (Potions, Grenades) usable in combat via Bonus Action
+- **Interaction:** Drag-and-drop from Stash to Slot. Invalid slots highlight Red. Valid slots highlight Green
+
+### **B. The Stash (Grid View)**
+
+- **Visual:** A grid of item tiles on the right side of the screen
+- **Filters:** Tabs for [All] [Weapons] [Armor] [Utility] [Consumables]
+- **Sorting:** By Rarity (Color Coded border) or Tier
+- **Context Menu:** Right-click an item to "Equip to Active Unit" or "Salvage/Sell"
+
+## **2. TypeScript Interfaces (Data Model)**
+
+```typescript
+// src/types/Inventory.ts
+
+export type ItemType =
+ | "WEAPON"
+ | "ARMOR"
+ | "RELIC"
+ | "UTILITY"
+ | "CONSUMABLE"
+ | "MATERIAL";
+export type Rarity = "COMMON" | "UNCOMMON" | "RARE" | "ANCIENT";
+export type SlotType = "MAIN_HAND" | "OFF_HAND" | "BODY" | "ACCESSORY" | "BELT";
+
+/**
+ * A specific instance of an item.
+ * Allows for RNG stats or durability in the future.
+ */
+export interface ItemInstance {
+ uid: string; // Unique Instance ID (e.g. "ITEM_12345_A")
+ defId: string; // Reference to static registry (e.g. "ITEM_RUSTY_BLADE")
+ isNew: boolean; // For UI "New!" badges
+ quantity: number; // For stackables (Potions/Materials)
+}
+
+/**
+ * The inventory of a single character.
+ */
+export interface UnitLoadout {
+ mainHand: ItemInstance | null;
+ offHand: ItemInstance | null;
+ body: ItemInstance | null;
+ accessory: ItemInstance | null;
+ belt: [ItemInstance | null, ItemInstance | null]; // Fixed 2 slots
+}
+
+/**
+ * The shared storage (Run Bag or Hub Stash).
+ */
+export interface InventoryStorage {
+ id: string; // "RUN_LOOT" or "HUB_VAULT"
+ items: ItemInstance[]; // Unordered list
+ currency: {
+ aetherShards: number;
+ ancientCores: number;
+ };
+}
+
+/**
+ * Data payload for moving items.
+ */
+export interface TransferRequest {
+ itemUid: string;
+ sourceContainer: "STASH" | "UNIT_LOADOUT";
+ targetContainer: "STASH" | "UNIT_LOADOUT";
+ targetSlot?: SlotType; // If moving to Unit
+ slotIndex?: number; // For Belt slots (0 or 1)
+ unitId?: string; // Which unit owns the loadout
+}
+```
+
+## **3. Logic & Rules**
+
+### **A. Equipping Items**
+
+1. **Validation:**
+ - Check `Item.requirements` (Class Lock, Min Stats) against `Unit.baseStats`
+ - Check Slot Compatibility (Can't put Armor in Weapon slot)
+ - _Two-Handed Logic:_ If equipping a 2H weapon, unequip Off-Hand automatically
+2. **Swapping:**
+ - If target slot is occupied, move the existing item to Stash (or Swap if source was another slot)
+3. **Stat Recalculation:**
+ - Trigger `unit.recalculateStats()` immediately
+
+### **B. Stacking**
+
+- **Equipment:** Non-stackable. Each Sword is a unique instance
+- **Consumables/Materials:** Stackable up to 99
+- **Logic:** When adding a Potion to Stash, check if `defId` exists. If yes, `quantity++`. If no, create new entry
+
+### **C. The Extraction (End of Run)**
+
+```js
+function finalizeRun(runInventory, hubInventory) {
+ // 1. Transfer Currency
+ hubInventory.currency.shards += runInventory.currency.shards;
+
+ // 2. Transfer Items
+ for (let item of runInventory.items) {
+ hubInventory.addItem(item);
+ }
+
+ // 3. Clear Run Inventory
+ runInventory.clear();
+}
+```
+
+## **4. Conditions of Acceptance (CoA)**
+
+**CoA 1: Class Restrictions**
+
+- Attempting to equip a "Tinker Only" item on a "Vanguard" must fail
+- The UI should visually dim incompatible items when a unit is selected
+
+**CoA 2: Stat Updates**
+
+- Equipping a `+5 Attack` sword must immediately update the displayed Attack stat in the Character Sheet
+- Unequipping it must revert the stat
+
+**CoA 3: Belt Logic**
+
+- Using a Consumable in combat (via `ActionSystem`) must reduce its quantity
+- If quantity reaches 0, the item reference is removed from the Belt slot
+
+**CoA 4: Persistence**
+
+- Saving the game must serialize the `InventoryStorage` array correctly
+- Loading the game must restore specific item instances (not just generic definitions)
+
+## **5. Integration Strategy (Wiring)**
+
+### **A. Game Loop (Looting)**
+
+- **Trigger:** Player unit moves onto a tile with a Loot Chest / Item Drop
+- **Logic:** `GameLoop` detects collision -> calls `InventoryManager.runStash.addItem(foundItem)`
+- **Visual:** `VoxelManager` removes the chest model. UI shows "Item Acquired" toast
+
+### **B. Character Sheet (Management)**
+
+- **Trigger:** Player opens Character Sheet -> Inventory Tab
+- **Logic:** The UI Component imports `InventoryManager`
+- **Display:** It renders `InventoryManager.runStash.items` (if in dungeon) or `hubStash.items` (if in hub)
+- **Action:** Dragging an item to a slot calls `InventoryManager.equipItem(activeUnit, itemUid, slot)`
+
+### **C. Combat System (Consumables)**
+
+- **Trigger:** Player selects a "Potion" from the Combat HUD Action Bar
+- **Logic:**
+ 1. Check `unit.equipment.belt` for the item
+ 2. Execute Effect (Heal)
+ 3. Call `InventoryManager.consumeItem(unit, slotIndex)`
+ 4. Update Unit Inventory state
+
+### **D. Persistence (Saving)**
+
+- **Trigger:** `GameStateManager.saveRun()` or `saveRoster()`
+- **Logic:** The `Explorer` class's `equipment` object and the `InventoryManager`'s `runStash` must be serialized to JSON
+- **Requirement:** Ensure `ItemInstance` objects are saved with their specific `uid` and `quantity`, not just `defId`
+
diff --git a/.cursor/rules/data/RULE.md b/.cursor/rules/data/RULE.md
new file mode 100644
index 0000000..5bdc327
--- /dev/null
+++ b/.cursor/rules/data/RULE.md
@@ -0,0 +1,23 @@
+---
+description: Standards for JSON data structures (Classes, Items, Missions).
+globs: assets/data/\*\*/\*.json**
+---
+
+# **Data Schema Standards**
+
+## **Itemization**
+
+- **IDs:** ALL CAPS SNAKE_CASE (e.g., ITEM_RUSTY_BLADE).
+- **Files:** One file per item/class OR grouped by category. Consistent keys are mandatory.
+- **Effects:** Use passive_effects array for event listeners and active_ability for granted skills.
+
+## **Missions**
+
+- **Structure:** Must match the Mission TypeScript interface.
+- **Narrative:** Link to narrative/ JSON files via ID. Do not embed full dialogue in Mission JSON.
+- **Rewards:** Must specify guaranteed vs conditional.
+
+## **Classes**
+
+- **Skill Trees:** Use the standard 30-node topology template.
+- **Growth:** Define base_stats (Lvl 1\) and growth_rates.
diff --git a/.cursor/rules/generation/RULE.md b/.cursor/rules/generation/RULE.md
new file mode 100644
index 0000000..f3f576b
--- /dev/null
+++ b/.cursor/rules/generation/RULE.md
@@ -0,0 +1,22 @@
+---
+description: Standards for map generation, seeding, and textures.
+globs: src/generation/*.js**
+---
+
+# **Procedural Generation Standards**
+
+## **Core Principles**
+
+1. **Determinism:** All generators must accept a seed. Using the same seed must produce the exact same map and textures. Use src/utils/SeededRandom.js.
+2. **Additive vs Subtractive:**
+ - **Ruins:** Use Additive (Fill 0, Build Rooms).
+ - **Caves:** Use Subtractive/Cellular (Fill 1, Carve Caves).
+3. **Playability:**
+ - Always run PostProcessor.ensureConnectivity() to prevent soft-locks.
+ - Always ensure valid "Floor" tiles exist for spawning.
+
+## **Texture Generation**
+
+- Use OffscreenCanvas for texture generation to support Web Workers.
+- Return a composite object (diffuse, normal, roughness) for PBR materials.
+- Output textures to generatedAssets object, do not apply directly to Three.js materials inside the generator.
diff --git a/.cursor/rules/logic/CombatSkillUsage/RULE.md b/.cursor/rules/logic/CombatSkillUsage/RULE.md
new file mode 100644
index 0000000..1266e4a
--- /dev/null
+++ b/.cursor/rules/logic/CombatSkillUsage/RULE.md
@@ -0,0 +1,155 @@
+---
+description: Combat skill usage workflow - selection, targeting, and execution of active skills
+globs: src/systems/SkillTargetingSystem.js, src/systems/SkillTargetingSystem.ts, src/core/GameLoop.js
+alwaysApply: false
+---
+
+# **Combat Skill Usage Rule**
+
+This rule defines the workflow for selecting, targeting, and executing Active Skills during the Combat Phase.
+
+## **1. The Interaction Flow**
+
+The process follows a strict 3-step sequence:
+
+1. **Selection (UI):** Player clicks a skill button in the HUD
+2. **Targeting (Grid):** Game enters `TARGETING_MODE`. Player moves cursor to select a target. Valid targets are highlighted
+3. **Execution (Engine):** Player confirms selection. Costs are paid, and effects are applied
+
+## **2. State Machine Updates**
+
+We need to expand the CombatState in GameLoop to handle targeting.
+
+| State | Description | Input Behavior |
+| :------------------------ | :---------------------------------------------------------- | :------------------------------------------------------------------------------------------- |
+| **IDLE / SELECTING_MOVE** | Standard state. Cursor highlights movement range. | Click Unit = Select. Click Empty = Move. |
+| **TARGETING_SKILL** | Player has selected a skill. Cursor highlights Skill Range. | **Hover:** Update AoE Reticle. **Click:** Execute Skill. **Cancel (B/Esc):** Return to IDLE. |
+| **EXECUTING_SKILL** | Animation playing. Input locked. | None. |
+
+## **3. The Skill Targeting System**
+
+We need a helper system (`src/systems/SkillTargetingSystem.js`) to handle the complex math of "Can I hit this?" without cluttering the GameLoop.
+
+### **Core Logic: isValidTarget(source, targetTile, skillDef)**
+
+1. **Range Check:** Manhattan distance between Source and Target <= `skill.range`
+2. **Line of Sight (LOS):** Raycast from Source head height to Target center. Must not hit `isSolid` voxels (unless skill has `ignore_cover`)
+3. **Content Check:**
+ - If `target_type` is **ENEMY**: Tile must contain a unit AND `unit.team != source.team`
+ - If `target_type` is **ALLY**: Tile must contain a unit AND `unit.team == source.team`
+ - If `target_type` is **EMPTY**: Tile must be empty
+
+### **Visual Logic: getAffectedTiles(targetTile, skillDef)**
+
+Calculates the Area of Effect (AoE) to highlight in **Red**.
+
+- **SINGLE:** Just the target tile
+- **CIRCLE (Radius R):** All tiles within distance R of target
+- **LINE (Length L):** Raycast L tiles in the cardinal direction from Source to Target
+- **CONE:** A triangle pattern originating from Source
+
+## **4. Integration Steps**
+
+### **Step 1: Create SkillTargetingSystem**
+
+This class encapsulates the math.
+
+```js
+class SkillTargetingSystem {
+ constructor(grid, unitManager) { ... }
+
+ /** Returns { valid: boolean, reason: string } */
+ validateTarget(sourceUnit, targetPos, skillId) { ... }
+
+ /** Returns array of {x,y,z} for highlighting */
+ getAoETiles(sourcePos, cursorPos, skillId) { ... }
+}
+```
+
+### **Step 2: Update GameLoop State**
+
+Add `activeSkillId` to track which skill is pending.
+
+```js
+// In GameLoop.js
+
+// 1. Handle UI Event
+onSkillClicked(skillId) {
+ // Validate unit has AP
+ if (this.unit.currentAP < getSkillCost(skillId)) return;
+
+ this.combatState = 'TARGETING_SKILL';
+ this.activeSkillId = skillId;
+
+ // VISUALS: Clear Movement Blue Grid -> Show Attack Red Grid (Range)
+ const skill = this.unit.skills.get(skillId);
+ this.voxelManager.highlightRange(this.unit.pos, skill.range, 'RED_OUTLINE');
+}
+
+// 2. Handle Cursor Hover (InputManager event)
+onCursorHover(pos) {
+ if (this.combatState === 'TARGETING_SKILL') {
+ const aoeTiles = this.targetingSystem.getAoETiles(this.unit.pos, pos, this.activeSkillId);
+ this.voxelManager.showReticle(aoeTiles); // Solid Red Highlight
+ }
+}
+```
+
+### **Step 3: Execution Logic**
+
+When the player confirms the click.
+
+```js
+// In GameLoop.js -> triggerSelection()
+
+if (this.combatState === 'TARGETING_SKILL') {
+ const valid = this.targetingSystem.validateTarget(this.unit, cursor, this.activeSkillId);
+
+ if (valid) {
+ this.executeSkill(this.activeSkillId, cursor);
+ } else {
+ // Audio: Error Buzz
+ console.log("Invalid Target");
+ }
+}
+
+executeSkill(skillId, targetPos) {
+ this.combatState = 'EXECUTING_SKILL';
+
+ // 1. Deduct Costs (AP, Cooldown) via SkillManager
+ this.unit.skillManager.payCosts(skillId);
+
+ // 2. Get Targets (Units in AoE)
+ const targets = this.targetingSystem.getUnitsInAoE(targetPos, skillId);
+
+ // 3. Process Effects (Damage, Status) via EffectProcessor
+ const skillDef = this.registry.get(skillId);
+ skillDef.effects.forEach(eff => {
+ targets.forEach(t => this.effectProcessor.process(eff, this.unit, t));
+ });
+
+ // 4. Cleanup
+ this.combatState = 'IDLE';
+ this.activeSkillId = null;
+ this.voxelManager.clearHighlights();
+}
+```
+
+## **5. Conditions of Acceptance (CoA)**
+
+**CoA 1: Range Validation**
+
+- A skill with `range: 3` must reject targets beyond 3 tiles (Manhattan distance)
+
+**CoA 2: Line of Sight**
+
+- A skill targeting an enemy behind a wall must fail the LOS check
+
+**CoA 3: Cost Payment**
+
+- Executing a skill must deduct AP and increment cooldown before effects are applied
+
+**CoA 4: State Cleanup**
+
+- After skill execution, the game must return to `IDLE` state and clear all targeting highlights
+
diff --git a/.cursor/rules/logic/CombatState/RULE.md b/.cursor/rules/logic/CombatState/RULE.md
new file mode 100644
index 0000000..4f88173
--- /dev/null
+++ b/.cursor/rules/logic/CombatState/RULE.md
@@ -0,0 +1,101 @@
+---
+description: Combat state and movement logic for turn-based combat loop
+globs: src/systems/TurnSystem.js, src/systems/MovementSystem.js, src/types/CombatState.ts, src/types/Actions.ts
+alwaysApply: false
+---
+
+# **Combat State & Movement Rule**
+
+This rule defines the logic for managing the Turn-Based Combat Loop and the execution of Movement Actions.
+
+## **1. TypeScript Interfaces (Data Models)**
+
+```typescript
+export type CombatPhase =
+ | "INIT"
+ | "TURN_START"
+ | "WAITING_FOR_INPUT"
+ | "EXECUTING_ACTION"
+ | "TURN_END"
+ | "COMBAT_END";
+
+export interface CombatState {
+ /** Whether combat is currently active */
+ isActive: boolean;
+ /** Current Round number */
+ round: number;
+ /** Ordered list of Unit IDs for the current round */
+ turnQueue: string[];
+ /** The ID of the unit currently taking their turn */
+ activeUnitId: string | null;
+ /** Current phase of the turn loop */
+ phase: CombatPhase;
+}
+
+// src/types/Actions.ts
+
+export interface ActionRequest {
+ type: "MOVE" | "SKILL" | "ITEM" | "WAIT";
+ sourceId: string;
+ targetId?: string; // For targeted skills
+ targetPosition?: { x: number; y: number; z: number }; // For movement/AoE
+ skillId?: string;
+ itemId?: string;
+}
+
+export interface MovementResult {
+ success: boolean;
+ path: { x: number; y: number; z: number }[];
+ costAP: number;
+ finalPosition: { x: number; y: number; z: number };
+}
+```
+
+## **2. Conditions of Acceptance (CoA)**
+
+These checks ensure the combat loop feels fair and responsive.
+
+### **System: TurnSystem (src/systems/TurnSystem.js)**
+
+- **CoA 1: Initiative Roll:** Upon starting combat, all active units must be sorted into the turnQueue based on their Speed stat (Highest First)
+- **CoA 2: Turn Start Hygiene:** When a unit's turn begins:
+ - Their `currentAP` must reset to `baseAP`
+ - Status effects (DoTs) must tick
+ - Cooldowns must decrement
+- **CoA 3: Cycling:** Calling `endTurn()` must move the `activeUnitId` to the next in the queue. If the queue is empty, increment round and re-roll/reset the queue
+
+### **System: MovementSystem (src/systems/MovementSystem.js)**
+
+- **CoA 1: Validation:** Moving to a tile must fail if:
+ - The tile is blocked/occupied
+ - No path exists
+ - The unit has insufficient AP for the _entire_ path
+- **CoA 2: Execution:** A successful move must:
+ - Update the Unit's position in the UnitManager
+ - Update the VoxelGrid occupancy map
+ - Deduct the correct AP cost (including terrain modifiers)
+- **CoA 3: Path Snapping:** If the user clicks a tile, but the unit only has AP to reach halfway, the system should allow moving to the _furthest reachable tile_ on that path (optional QoL)
+
+## **3. Implementation Requirements**
+
+### **The Turn System**
+
+Create `src/systems/TurnSystem.js`. It should manage the CombatState.
+
+1. Implement `startCombat(units)`: Sorts units by speed into turnQueue and sets phase to TURN_START
+2. Implement `startTurn()`: Refills the active unit's AP, processes cooldowns/statuses, and sets phase to WAITING_FOR_INPUT
+3. Implement `endTurn()`: Rotates the queue. If queue is empty, start new Round
+4. It should accept the UnitManager in the constructor to access unit stats
+5. Dispatch events: `combat-start`, `turn-start`, `turn-end`
+
+### **The Movement System**
+
+Create `src/systems/MovementSystem.js`. It coordinates Pathfinding, VoxelGrid, and UnitManager.
+
+1. Implement `validateMove(unit, targetPos)`: Returns `{ valid: boolean, cost: number, path: [] }`. It checks `A*` pathfinding and compares cost vs `unit.currentAP`
+2. Implement `executeMove(unit, targetPos)`:
+ - Validates the move first
+ - Updates `grid.moveUnit(unit, targetPos)`
+ - Deducts AP
+ - Returns a Promise that resolves when the visual movement (optional animation hook) would handle it, or immediately for logic
+
diff --git a/.cursor/rules/logic/EffectProcessor/RULE.md b/.cursor/rules/logic/EffectProcessor/RULE.md
new file mode 100644
index 0000000..5a76623
--- /dev/null
+++ b/.cursor/rules/logic/EffectProcessor/RULE.md
@@ -0,0 +1,174 @@
+---
+description: Effect Processor architecture - the central system for executing all game state changes
+globs: src/systems/EffectProcessor.js, src/systems/EffectProcessor.ts, src/types/Effects.ts
+alwaysApply: false
+---
+
+# **Effect Processor Rule**
+
+The EffectProcessor is the central system responsible for executing all changes to the game state (Damage, Healing, Movement, Spawning). It is a stateless logic engine that takes a **Definition** (What to do) and a **Context** (Who is doing it to whom), and applies the necessary mutations to the UnitManager or VoxelGrid.
+
+## **1. System Overview**
+
+### **Architectural Role**
+
+- **Input:** EffectDefinition (JSON), Source (Unit), Target (Unit/Tile)
+- **Output:** State Mutation (HP changed, Unit moved) + EffectResult (Log data)
+- **Pattern:** Strategy Pattern. Each effect_type maps to a specific Handler Function
+
+## **2. Integration Points**
+
+### **A. Calling the Processor**
+
+The Processor is never called directly by the UI. It is invoked by:
+
+1. **SkillManager:** When an Active Skill is executed
+2. **EventSystem:** When a Passive Item triggers (e.g., "On Hit -> Apply Burn")
+3. **Environmental Hazard:** When a unit starts their turn on Fire/Acid
+
+### **B. Dependencies**
+
+The Processor requires injection of:
+
+- VoxelGrid: To check collision, modify terrain, or move units
+- UnitManager: To find neighbors (Chain Lightning) or spawn tokens (Turrets)
+- RNG: A seeded random number generator for damage variance and status chances
+
+## **3. Data Structure (JSON Schema)**
+
+Every effect in the game must adhere to this structure. See `src/types/Effects.ts` for full TypeScript definitions.
+
+### **Effect Types**
+
+```typescript
+export type EffectType =
+ // Combat
+ | "DAMAGE"
+ | "HEAL"
+ | "CHAIN_DAMAGE"
+ | "REDIRECT_DAMAGE"
+ | "PREVENT_DEATH"
+ // Status & Stats
+ | "APPLY_STATUS"
+ | "REMOVE_STATUS"
+ | "REMOVE_ALL_DEBUFFS"
+ | "APPLY_BUFF"
+ | "GIVE_AP"
+ | "ADD_CHARGE"
+ | "ADD_SHIELD"
+ | "CONVERT_DAMAGE_TO_HEAL"
+ | "DYNAMIC_BUFF"
+ // Movement & Physics
+ | "TELEPORT"
+ | "MOVE_TO_TARGET"
+ | "SWAP_POSITIONS"
+ | "PHYSICS_PULL"
+ | "PUSH"
+ // World & Spawning
+ | "SPAWN_OBJECT"
+ | "SPAWN_HAZARD"
+ | "SPAWN_LOOT"
+ | "MODIFY_TERRAIN"
+ | "DESTROY_VOXEL"
+ | "DESTROY_OBJECTS"
+ | "REVEAL_OBJECTS"
+ | "COLLECT_LOOT"
+ // Meta / Logic
+ | "REPEAT_SKILL"
+ | "CANCEL_EVENT"
+ | "REDUCE_COST"
+ | "BUFF_SPAWN"
+ | "MODIFY_AOE";
+```
+
+### **Effect Parameters**
+
+```typescript
+export interface EffectParams {
+ // Combat Magnitude
+ power?: number; // Base amount (Damage/Heal)
+ attribute?: string; // Stat to scale off (e.g., "strength", "magic")
+ scaling?: number; // Multiplier for attribute (Default: 1.0)
+ element?: "PHYSICAL" | "FIRE" | "ICE" | "SHOCK" | "VOID" | "TECH";
+
+ // Chaining
+ bounces?: number;
+ decay?: number;
+ synergy_trigger?: string; // Status ID that triggers bonus effect
+
+ // Status/Buffs
+ status_id?: string;
+ duration?: number;
+ stat?: string; // For Buffs
+ value?: number; // For Buffs/Mods
+ chance?: number; // 0.0 to 1.0 (Proc chance)
+
+ // Physics
+ force?: number; // Distance
+ destination?: "TARGET" | "BEHIND_TARGET" | "ADJACENT_TO_TARGET";
+
+ // World
+ object_id?: string; // Unit ID to spawn
+ hazard_id?: string;
+ tag?: string; // Filter for objects (e.g. "COVER")
+ range?: number; // AoE radius
+
+ // Logic
+ percentage?: number; // 0.0 - 1.0
+ amount?: number; // Flat amount (AP/Charge)
+ amount_range?: [number, number]; // [min, max]
+ set_hp?: number; // Hard set HP value
+ shape?: "CIRCLE" | "LINE" | "CONE" | "SINGLE";
+ size?: number;
+ multiplier?: number;
+}
+```
+
+## **4. Handler Specifications**
+
+### **Handler: DAMAGE**
+
+- **Logic:** `FinalDamage = (BasePower + (Source[Attribute] * Scaling)) - Target.Defense`
+- **Element Check:** If Target has Resistance/Weakness to `element`, modify FinalDamage
+- **Result:** `Target.currentHP -= FinalDamage`
+
+### **Handler: CHAIN_DAMAGE**
+
+- **Logic:** Apply `DAMAGE` to primary target. Then, scan for N nearest enemies within Range R. Apply `DAMAGE * Decay` to them
+- **Synergy:** If `condition.target_status` is present on a target, the chain may branch or deal double damage
+
+### **Handler: TELEPORT**
+
+- **Logic:** Validate destination tile (must be Air and Unoccupied). Update `Unit.position` and `VoxelGrid.unitMap`
+- **Visuals:** Trigger "Vanish" VFX at old pos, "Appear" VFX at new pos
+
+### **Handler: MODIFY_TERRAIN**
+
+- **Logic:** Update `VoxelGrid` ID at Target coordinates
+- **Use Case:** Sapper's "Breach Charge" turns `ID_WALL` into `ID_AIR`
+- **Safety:** Check `VoxelGrid.isDestructible()`. Do not destroy bedrock
+
+## **5. Conditions of Acceptance (CoA)**
+
+**CoA 1: Attribute Scaling**
+
+- Given a Damage Effect with `power: 10` and `attribute: "magic"`, if Source has `magic: 5`, the output damage must be 15
+
+**CoA 2: Conditional Logic**
+
+- Given an Effect with `condition: { target_status: "WET" }`, if the target does _not_ have the "WET" status, the effect must **not** execute (return early)
+
+**CoA 3: State Mutation**
+
+- When `APPLY_STATUS` is executed, the Target unit's `statusEffects` array must contain the new ID with the correct duration
+
+**CoA 4: Physics Safety**
+
+- When `PUSH` is executed, the system must check `VoxelGrid.isSolid()` behind the target. If a wall exists, the unit must **not** move into the wall (optionally take "Smash" damage instead)
+
+## **6. Implementation Requirements**
+
+- **Statelessness:** The processor should not hold state. It acts on the Unit and Grid passed to it
+- **Schema:** Effects must adhere to the EffectDefinition interface (Type + Params)
+- **All game state mutations** (Damage, Move, Spawn) **MUST** go through `EffectProcessor.process()`
+
diff --git a/.cursor/rules/logic/RULE.md b/.cursor/rules/logic/RULE.md
new file mode 100644
index 0000000..01fa210
--- /dev/null
+++ b/.cursor/rules/logic/RULE.md
@@ -0,0 +1,23 @@
+---
+description: Standards for gameplay logic, AI, and Effect processing.
+globs: src/systems/*.js, src/managers/*.js**
+---
+
+# **Logic Systems Standards**
+
+## **Effect Processor**
+
+- **The Rulebook:** All game state mutations (Damage, Move, Spawn) **MUST** go through EffectProcessor.process().
+- **Statelessness:** The processor should not hold state. It acts on the Unit and Grid passed to it.
+- **Schema:** Effects must adhere to the EffectDefinition interface (Type \+ Params).
+
+## **AI Controller**
+
+- **Pattern:** Utility-Based Decision Making.
+- **Execution:** AI does not cheat. It must use the same Pathfinding and SkillManager checks as the player.
+- **Archetypes:** Logic must be driven by AIBehaviorProfile (e.g., Bruiser vs Kiter).
+
+## **Unit Manager**
+
+- **Role:** Single Source of Truth for "Who is alive?".
+- **Queries:** Use getUnitsInRange() for spatial lookups. Do not iterate the Grid manually to find units.
diff --git a/.cursor/rules/logic/TurnLifecycle/RULE.md b/.cursor/rules/logic/TurnLifecycle/RULE.md
new file mode 100644
index 0000000..d41837d
--- /dev/null
+++ b/.cursor/rules/logic/TurnLifecycle/RULE.md
@@ -0,0 +1,115 @@
+---
+description: Turn lifecycle logic - activation, reset, and tick system for unit turns
+globs: src/systems/TurnSystem.js, src/systems/TurnSystem.ts
+alwaysApply: false
+---
+
+# **Turn Lifecycle Rule**
+
+This rule defines the exact state changes that occur when a unit becomes active (Start Turn) and when they finish (End Turn).
+
+## **1. Start of Turn (Activation Phase)**
+
+This logic runs immediately when TurnSystem identifies a unit as the new active actor.
+
+### **A. Action Point (AP) Regeneration**
+
+The unit must be given their budget for the turn.
+
+- **Formula:** Base AP (3) + Math.floor(Speed / 5)
+- **Constraint:** AP does _not_ roll over. It resets to this max value every turn. This encourages players to use their actions rather than hoard them
+
+### **B. Cooldown Reduction**
+
+- Iterate through all skills in `unit.skills`
+- If `cooldown > 0`, decrement by 1
+- _Note:_ This ensures a skill used on Turn 1 with a 1-turn cooldown is ready again on Turn 2
+
+### **C. Status Effect Tick (The "Upkeep" Step)**
+
+- Iterate through `unit.statusEffects`
+- **Apply Effect:** If the effect is a "DoT" (Damage over Time) or "HoT" (Heal over Time), apply the value now
+- **Decrement Duration:** Reduce the effect's duration by 1
+- **Expire:** If duration reaches 0, remove the effect immediately (unless it is "Permanent")
+- **Stun Check:** If a STUN status is active, skip the Action Phase and immediately trigger **End Turn**
+
+## **2. End of Turn (Resolution Phase)**
+
+This logic runs when the player clicks "End Turn" or the AI finishes its logic.
+
+### **A. Charge Meter Consumption**
+
+- **Logic:** We do _not_ set Charge to 0. We subtract 100
+- **Why?** If a unit has 115 Charge (because they are very fast), setting it to 0 deletes that extra 15 speed advantage. Subtracting 100 lets them keep the 15 head-start on the next race
+- **Formula:** `unit.chargeMeter = Math.max(0, unit.chargeMeter - 100)`
+
+### **B. Action Slot Reset**
+
+- Reset flags like `hasMoved`, `hasAttacked`, or `standardActionUsed` to false so the UI is clean for the next time they act
+
+## **3. The Tick Loop (The "Race")**
+
+This runs whenever no unit is currently active.
+
+```js
+while (no_unit_has_100_charge) {
+ globalTick++;
+ for (all_units as unit) {
+ // Gain Charge
+ unit.chargeMeter += unit.stats.speed;
+
+ // Cap?
+ // No cap, but we check for >= 100 break condition
+ }
+}
+// Sort by Charge (Descending) -> Highest Charge wins
+```
+
+## **4. TypeScript Interfaces**
+
+```typescript
+// src/types/TurnSystem.ts
+
+export interface TurnState {
+ /** The ID of the unit currently acting */
+ activeUnitId: string | null;
+ /** How many "Ticks" have passed in total (Time) */
+ globalTime: number;
+ /** Ordered list of who acts next (predicted) for the UI */
+ projectedQueue: string[];
+}
+
+export interface TurnEvent {
+ type: 'TURN_CHANGE';
+ previousUnitId: string;
+ nextUnitId: string;
+ /** Did we wrap around a "virtual round"? */
+ isNewRound: boolean;
+}
+```
+
+## **5. Conditions of Acceptance (CoA)**
+
+**CoA 1: Speed determines frequency**
+
+- If Unit A has Speed 20 and Unit B has Speed 10:
+- Unit A should act roughly twice as often as Unit B over 10 turns
+
+**CoA 2: Queue Prediction**
+
+- The system must expose a `getPredictedQueue(depth)` method that "simulates" future ticks without applying them, so the UI can show the "Next 5 Units" list correctly
+
+**CoA 3: Status Duration**
+
+- A Status Effect with `duration: 1` applied on Turn X must expire exactly at the _start_ of the unit's _next_ turn (Turn X+1), ensuring it affects them for one full action phase
+
+## **6. Implementation Requirements**
+
+Create `src/systems/TurnSystem.js`:
+
+1. **State:** Maintain a `globalTick` counter and reference to UnitManager
+2. **End Turn Logic:** Implement `endTurn(unit)`. Reset the unit's charge to 0. Tick their cooldowns/statuses
+3. **Time Loop:** Implement `advanceToNextTurn()`. Loop through all alive units, adding Speed to Charge. Stop as soon as one or more units reach 100
+4. **Tie Breaking:** If multiple units pass 100 in the same tick, the one with the highest total charge goes first. If equal, Player beats Enemy
+5. **Prediction:** Implement `simulateQueue(depth)` which clones the current charge state and runs the loop virtually to return an array of the next depth Unit IDs
+
diff --git a/.cursor/rules/logic/TurnSystem/RULE.md b/.cursor/rules/logic/TurnSystem/RULE.md
new file mode 100644
index 0000000..7927b3a
--- /dev/null
+++ b/.cursor/rules/logic/TurnSystem/RULE.md
@@ -0,0 +1,77 @@
+---
+description: Turn resolution specification - the tick system for transitioning between unit turns
+globs: src/systems/TurnSystem.js, src/systems/TurnSystem.ts, src/types/TurnSystem.ts
+alwaysApply: false
+---
+
+# **Turn Resolution Rule**
+
+This rule defines the logic that occurs the moment a unit presses "End Turn". It transitions the game from one actor to the next using a time-based simulation.
+
+## **1. The Logic Flow**
+
+When `endTurn()` is called:
+
+1. **Resolution (Old Unit):**
+ - **Cooldowns:** Decrement cooldowns on all skills by 1
+ - **Status Effects:** Tick durations of active effects (Buffs/Debuffs). Remove expired ones
+ - **Charge Reset:** The unit's `chargeMeter` is reset to 0 (or reduced by action cost)
+2. **Time Advancement (The "Tick"):**
+ - The system enters a `while(no_active_unit)` loop
+ - **Tick:** Every unit gains `Charge += Speed`
+ - **Check:** Does anyone have `Charge >= 100`?
+ - _Yes:_ Stop looping. The unit with the highest Charge is the **New Active Unit**
+ - _No:_ Continue looping
+3. **Activation (New Unit):**
+ - **AP Refill:** Set `currentAP` to `maxAP`
+ - **Start Triggers:** Fire `ON_TURN_START` events (e.g., "Take Poison Damage")
+ - **Input Unlock:** If Player, unlock UI. If Enemy, trigger AI
+
+## **2. TypeScript Interfaces**
+
+```typescript
+// src/types/TurnSystem.ts
+
+export interface TurnState {
+ /** The ID of the unit currently acting */
+ activeUnitId: string | null;
+ /** How many "Ticks" have passed in total (Time) */
+ globalTime: number;
+ /** Ordered list of who acts next (predicted) for the UI */
+ projectedQueue: string[];
+}
+
+export interface TurnEvent {
+ type: 'TURN_CHANGE';
+ previousUnitId: string;
+ nextUnitId: string;
+ /** Did we wrap around a "virtual round"? */
+ isNewRound: boolean;
+}
+```
+
+## **3. Conditions of Acceptance (CoA)**
+
+**CoA 1: Speed determines frequency**
+
+- If Unit A has Speed 20 and Unit B has Speed 10:
+- Unit A should act roughly twice as often as Unit B over 10 turns
+
+**CoA 2: Queue Prediction**
+
+- The system must expose a `getPredictedQueue(depth)` method that "simulates" future ticks without applying them, so the UI can show the "Next 5 Units" list correctly
+
+**CoA 3: Status Duration**
+
+- A Status Effect with `duration: 1` applied on Turn X must expire exactly at the _start_ of the unit's _next_ turn (Turn X+1), ensuring it affects them for one full action phase
+
+## **4. Implementation Requirements**
+
+Create `src/systems/TurnSystem.js`:
+
+1. **State:** Maintain a `globalTick` counter and reference to UnitManager
+2. **End Turn Logic:** Implement `endTurn(unit)`. Reset the unit's charge to 0. Tick their cooldowns/statuses
+3. **Time Loop:** Implement `advanceToNextTurn()`. Loop through all alive units, adding Speed to Charge. Stop as soon as one or more units reach 100
+4. **Tie Breaking:** If multiple units pass 100 in the same tick, the one with the highest total charge goes first. If equal, Player beats Enemy
+5. **Prediction:** Implement `simulateQueue(depth)` which clones the current charge state and runs the loop virtually to return an array of the next depth Unit IDs
+
diff --git a/.cursor/rules/ui/CharacterSheet/RULE.md b/.cursor/rules/ui/CharacterSheet/RULE.md
new file mode 100644
index 0000000..7a0d127
--- /dev/null
+++ b/.cursor/rules/ui/CharacterSheet/RULE.md
@@ -0,0 +1,116 @@
+---
+description: Character Sheet UI component - the Explorer's dossier combining stats, inventory, and skill tree
+globs: src/ui/components/CharacterSheet.js, src/ui/components/CharacterSheet.ts, src/types/CharacterSheet.ts
+alwaysApply: false
+---
+
+# **Character Sheet Rule**
+
+The Character Sheet is a UI component used to view and manage an Explorer unit. It combines Stat visualization, Inventory management (Paper Doll), and Skill Tree progression into a single tabbed interface.
+
+## **1. Visual Layout**
+
+Style: High-tech/Fantasy hybrid. Dark semi-transparent backgrounds with voxel-style borders.
+Container: Centered Modal (80% width/height).
+
+### **A. Header (The Identity)**
+
+- **Left:** Large 2D Portrait of the Unit
+- **Center:** Name, Class Title (e.g., "Vanguard"), and Level
+- **Bottom:** XP Bar (Gold progress fill). Displays "SP: [X]" badge if Skill Points are available
+- **Right:** "Close" button (X)
+
+### **B. Left Panel: Attributes (The Data)**
+
+- A vertical list of stats derived from `getEffectiveStat()`
+- **Primary:** Health (Bar), AP (Icons)
+- **Secondary:** Attack, Defense, Magic, Speed, Willpower, Tech
+- **Interaction:** Hovering a stat shows a Tooltip breaking down the value (Base + Gear + Buffs)
+
+### **C. Center Panel: Paper Doll (The Gear)**
+
+- **Visual:** The Unit's 3D model (or 2D silhouette) in the center
+- **Slots:** Four large square buttons arranged around the body:
+ - **Left:** Primary Weapon
+ - **Right:** Off-hand / Relic
+ - **Body:** Armor
+ - **Accessory:** Utility Device
+- **Interaction:** Clicking a slot opens the "Inventory Side-Panel" filtering for that slot type
+
+### **D. Right Panel: Tabs (The Management)**
+
+A tabbed container switching between:
+
+1. **Inventory:** Grid of unequipped items in the squad's backpack
+2. **Skills:** Embeds the **Skill Tree** component (Vertical Scrolling)
+3. **Mastery:** (Hub Only) Shows progress toward unlocking Tier 2 classes
+
+## **2. TypeScript Interfaces (Data Model)**
+
+```typescript
+// src/types/CharacterSheet.ts
+
+export interface CharacterSheetProps {
+ unitId: string;
+ readOnly: boolean; // True during enemy turn or restricted events
+}
+
+export interface CharacterSheetState {
+ unit: Explorer; // The full object
+ activeTab: 'INVENTORY' | 'SKILLS' | 'MASTERY';
+ selectedSlot: 'WEAPON' | 'ARMOR' | 'RELIC' | 'UTILITY' | null;
+}
+
+export interface StatTooltip {
+ label: string; // "Attack"
+ total: number; // 15
+ breakdown: { source: string, value: number }[]; // [{s: "Base", v: 10}, {s: "Rusty Blade", v: 5}]
+}
+```
+
+## **3. Conditions of Acceptance (CoA)**
+
+**CoA 1: Stat Rendering**
+
+- Stats must reflect the _effective_ value
+- If a unit has a "Weakness" debuff reducing Attack, the Attack number should appear Red. If buffed, Green
+
+**CoA 2: Equipment Swapping**
+
+- Clicking an Equipment Slot toggles the Right Panel to "Inventory" mode, filtered by that slot type
+- Clicking an item in the Inventory immediately equips it, swapping the old item back to the bag
+- Stats must verify/update immediately upon equip
+
+**CoA 3: Skill Interaction**
+
+- The Skill Tree tab must display the `SkillTreeUI` component we designed earlier
+- Spending an SP in the tree must subtract from the Unit's `skillPoints` and update the view immediately
+
+**CoA 4: Context Awareness**
+
+- In **Dungeon Mode**, the "Inventory" tab acts as the "Run Inventory" (temp loot)
+- In **Hub Mode**, the "Inventory" tab acts as the "Stash" (permanent items)
+
+## **4. Implementation Requirements**
+
+Create `src/ui/components/CharacterSheet.js` as a LitElement:
+
+1. **Layout:** Use CSS Grid to create the 3-column layout (Stats, Paper Doll, Tabs)
+2. **Props:** Accept a `unit` object. Watch for changes to re-render stats
+3. **Stats Column:** Implement a helper `_renderStat(label, value, breakdown)` that creates a hoverable div with a tooltip
+4. **Paper Doll:** Render 4 button slots. If slot is empty, show a ghost icon. If full, show the Item Icon
+5. **Tabs:** Implement simple switching logic
+ - _Inventory Tab:_ Render a grid of `item-card` elements
+ - _Skills Tab:_ Embed ``
+6. **Events:** Dispatch `equip-item` events when dragging/clicking inventory items
+
+## **5. Integration Strategy**
+
+**Context:** The Character Sheet is a modal that sits above all other UI (CombatHUD, Hub, TeamBuilder). It should be mounted to the `#ui-layer` when triggered and removed when closed.
+
+**Trigger Points:**
+
+- **Combat:** Clicking the Unit Portrait in CombatHUD dispatches `open-character-sheet`
+- **Hub:** Clicking a unit card in the Barracks dispatches `open-character-sheet`
+- **Input:** Pressing 'C' (configured in InputManager) triggers it for the active unit
+
diff --git a/.cursor/rules/ui/CombatHUD/RULE.md b/.cursor/rules/ui/CombatHUD/RULE.md
new file mode 100644
index 0000000..bc38bfb
--- /dev/null
+++ b/.cursor/rules/ui/CombatHUD/RULE.md
@@ -0,0 +1,140 @@
+---
+description: Combat HUD UI component - tactical interface overlay during combat phase
+globs: src/ui/components/CombatHUD.js, src/ui/components/CombatHUD.ts, src/types/CombatHUD.ts
+alwaysApply: false
+---
+
+# **Combat HUD Rule**
+
+The Combat HUD is the UI overlay active during the `GAME_RUN` / `ACTIVE` phase. It communicates turn order, unit status, and available actions to the player.
+
+## **1. Visual Description**
+
+**Layout:** A "Letterbox" style overlay that leaves the center of the screen clear for the 3D action.
+
+### **A. Top Bar (Turn & Status)**
+
+- **Turn Queue (Center-Left):** A horizontal list of circular portraits
+ - _Active Unit:_ Larger, highlighted with a gold border on the far left
+ - _Next Units:_ Smaller icons trailing to the right
+ - _Enemy Intent:_ If an enemy is active, a small icon (Sword/Shield) indicates their planned action type
+- **Global Info (Top-Right):**
+ - _Round Counter:_ "Round 3"
+ - _Threat Level:_ "High" (Color coded)
+
+### **B. Bottom Bar (The Dashboard)**
+
+- **Unit Status (Bottom-Left):**
+ - _Portrait:_ Large 2D art of the active unit
+ - _Bars:_ Health (Red), Action Points (Yellow), Charge (Blue)
+ - _Buffs:_ Small icons row above the bars
+- **Action Bar (Bottom-Center):**
+ - A row of interactive buttons for Skills and Items
+ - _Hotkeys:_ (1-5) displayed on the buttons
+ - _State:_ Buttons go grey if AP is insufficient or Cooldown is active
+ - _Tooltip:_ Hovering shows damage, range, and AP cost
+- **End Turn (Bottom-Right):**
+ - A prominent button to manually end the turn early (saving AP or Charge)
+
+### **C. Floating Elements (World Space)**
+
+- **Damage Numbers:** Pop up over units when hit
+- **Health Bars:** Small bars hovering over every unit in the 3D scene (billboarded)
+
+## **2. TypeScript Interfaces (Data Model)**
+
+```typescript
+// src/types/CombatHUD.ts
+
+export interface CombatState {
+ /** The unit currently taking their turn */
+ activeUnit: UnitStatus | null;
+
+ /** Sorted list of units acting next */
+ turnQueue: QueueEntry[];
+
+ /** Is the player currently targeting a skill? */
+ targetingMode: boolean;
+
+ /** Global combat info */
+ roundNumber: number;
+}
+
+export interface UnitStatus {
+ id: string;
+ name: string;
+ portrait: string;
+ hp: { current: number; max: number };
+ ap: { current: number; max: number };
+ charge: number; // 0-100
+ statuses: StatusIcon[];
+ skills: SkillButton[];
+}
+
+export interface QueueEntry {
+ unitId: string;
+ portrait: string;
+ team: 'PLAYER' | 'ENEMY';
+ /** 0-100 progress to next turn */
+ initiative: number;
+}
+
+export interface StatusIcon {
+ id: string;
+ icon: string; // URL or Emoji
+ turnsRemaining: number;
+ description: string;
+}
+
+export interface SkillButton {
+ id: string;
+ name: string;
+ icon: string;
+ costAP: number;
+ cooldown: number; // 0 = Ready
+ isAvailable: boolean; // True if affordable and ready
+}
+
+export interface CombatEvents {
+ 'skill-click': { skillId: string };
+ 'end-turn': void;
+ 'hover-skill': { skillId: string }; // For showing range grid
+}
+```
+
+## **3. Conditions of Acceptance (CoA)**
+
+**CoA 1: Real-Time Updates**
+
+- The HUD must update immediately when turn changes (via event listener)
+- Health bars must reflect current HP after damage is dealt
+- AP display must decrease when skills are used
+
+**CoA 2: Skill Availability**
+
+- Skills with insufficient AP must be visually disabled (greyed out)
+- Skills on cooldown must show remaining turns
+- Hovering a skill must highlight valid targets on the grid
+
+**CoA 3: Turn Queue Accuracy**
+
+- The queue must match the TurnSystem's predicted queue
+- Enemy units in queue must show their team indicator
+
+**CoA 4: Event Communication**
+
+- Clicking a skill button must dispatch `skill-click` event
+- Clicking "End Turn" must dispatch `end-turn` event
+- The HUD must not directly call GameLoop methods (event-driven only)
+
+## **4. Implementation Requirements**
+
+Create `src/ui/components/CombatHUD.js` as a LitElement:
+
+1. **Layout:** Use CSS Grid/Flexbox for letterbox layout (top bar, bottom bar, clear center)
+2. **Data Binding:** Subscribe to TurnSystem events (`turn-start`, `turn-end`) and GameLoop combat state
+3. **Turn Queue:** Render circular portraits in a horizontal row, highlighting the active unit
+4. **Action Bar:** Map unit's skills to buttons, showing AP cost and cooldown state
+5. **Event Handling:** Dispatch custom events for skill clicks and end turn actions
+6. **Responsive:** Support mobile (vertical stack) and desktop (horizontal layout)
+
diff --git a/.cursor/rules/ui/HubScreen/RULE.md b/.cursor/rules/ui/HubScreen/RULE.md
new file mode 100644
index 0000000..5857b58
--- /dev/null
+++ b/.cursor/rules/ui/HubScreen/RULE.md
@@ -0,0 +1,138 @@
+---
+description: Architecture, visuals, and integration logic for the HubScreen component - the persistent main menu of the campaign
+globs: src/ui/screens/HubScreen.js
+alwaysApply: false
+---
+
+# **HubScreen Component Rule**
+
+The HubScreen is the persistent "Main Menu" of the campaign where the player manages resources, units, and mission selection. It is a **View** that consumes data from the **GameStateManager** singleton.
+
+## **Integration Architecture**
+
+### **Data Sources**
+
+- **Wallet:** `gameStateManager.persistence.loadProfile().currency` (or similar cached state)
+- **Roster:** `gameStateManager.rosterManager.getDeployableUnits()`
+- **Unlocks:** Derived from `gameStateManager.missionManager.completedMissions`
+
+### **Navigation Flow**
+
+1. **Entry:** GameStateManager switches state to `STATE_META_HUB`. `index.html` mounts ``
+2. **Mission Selection:**
+ - User clicks "Mission Board"
+ - `` mounts `` overlay
+ - `` emits `mission-selected`
+3. **Squad Assembly:**
+ - `` catches event
+ - Transitions to `STATE_TEAM_BUILDER` (passing the selected Mission ID)
+4. **Deployment:**
+ - TeamBuilder emits `embark`
+ - GameStateManager handles embark (starts GameLoop)
+
+## **Visual Layout (The "Dusk Camp")**
+
+Style: 2.5D Parallax or Static Art background with UI overlays.
+Theme: A makeshift military camp at twilight. Torches, magical lanterns, supplies piled high.
+
+### **A. The Stage (Background)**
+
+- **Image:** `assets/images/ui/hub_bg_dusk.png` covers 100% width/height
+- **Hotspots:** Invisible, clickable divs positioned absolutely over key art elements
+ - _The Tent (Barracks):_ `top: 40%, left: 10%, width: 20%`. Hover: Glows Blue
+ - _The Table (Missions):_ `top: 60%, left: 40%, width: 20%`. Hover: Glows Gold
+ - _The Wagon (Market):_ `top: 50%, left: 80%, width: 15%`. Hover: Glows Green
+
+### **B. Top Bar (Status)**
+
+- **Left:** Game Logo
+- **Right:** Resource Strip
+ - `[💎 450 Shards] [⚙️ 12 Cores] [Day 4]`
+
+### **C. Bottom Dock (Navigation)**
+
+- A row of large, labeled buttons acting as redundant navigation
+- `[BARRACKS] [MISSIONS] [MARKET] [RESEARCH] [SYSTEM]`
+- **State:** Buttons are disabled/greyed out if the facility is locked
+
+### **D. Overlay Container (Modal Layer)**
+
+- A centralized div with a semi-transparent backdrop (`rgba(0,0,0,0.8)`) where sub-screens (Barracks, MissionBoard) are rendered without leaving the Hub
+
+## **TypeScript Interfaces**
+
+```typescript
+// src/types/HubInterfaces.ts
+
+export interface HubState {
+ wallet: {
+ aetherShards: number;
+ ancientCores: number;
+ };
+ rosterSummary: {
+ total: number;
+ ready: number;
+ injured: number;
+ };
+ unlocks: {
+ market: boolean;
+ research: boolean;
+ };
+ activeOverlay: "NONE" | "BARRACKS" | "MISSIONS" | "MARKET" | "SYSTEM";
+}
+
+export interface HubEvents {
+ // Dispatched when the player wants to leave the Hub context entirely
+ "request-team-builder": {
+ detail: { missionId: string };
+ };
+ "save-and-quit": void;
+}
+```
+
+## **Implementation Requirements**
+
+### **Component Structure**
+
+Create `src/ui/screens/HubScreen.js` as a LitElement:
+
+1. **Imports:** Import `gameStateManager` singleton and `html`, `css`, `LitElement`
+2. **State:** Define properties for `wallet`, `unlocks`, and `activeOverlay`
+3. **Lifecycle:** In `connectedCallback`, read `gameStateManager.persistence` to populate wallet/unlocks
+4. **Layout:**
+ - Background img covering host
+ - Absolute divs for Hotspots
+ - Flexbox header for Resources
+ - Flexbox footer for Dock Buttons
+ - Centered div for Overlays
+5. **Logic:**
+ - `_openOverlay(type)`: Sets state
+ - `_closeOverlay()`: Sets state to NONE
+ - `_onMissionSelected(e)`: Dispatches `request-team-builder` to window/parent
+6. **Lazy Loading:** Use dynamic imports inside the `render()` method
+
+## **Conditions of Acceptance (CoA)**
+
+**CoA 1: Live Data Binding**
+
+- On mount (`connectedCallback`), the component must fetch wallet and roster data from `gameStateManager`
+- The Top Bar must display the correct currency values
+
+**CoA 2: Hotspot & Dock Sync**
+
+- Clicking the "Mission Table" hotspot OR the "Missions" dock button must perform the same action: setting `activeOverlay = 'MISSIONS'`
+
+**CoA 3: Overlay Management**
+
+- When `activeOverlay` is `'MISSIONS'`, the `` component must be rendered in the Overlay Container
+- Clicking "Close" or "Back" inside an overlay must set `activeOverlay = 'NONE'`
+
+**CoA 4: Mission Handoff**
+
+- Selecting a mission in the Mission Board must dispatch an event that causes the Hub to dispatch `request-team-builder`, effectively handing control back to the App Router
+
+## **Event-Driven Architecture**
+
+- HubScreen must **never** directly call GameLoop or GameStateManager write operations
+- All state changes must be communicated via CustomEvents
+- Use `this.dispatchEvent(new CustomEvent('request-team-builder', { detail: { missionId } }))` for navigation
diff --git a/.cursor/rules/ui/RULE.md b/.cursor/rules/ui/RULE.md
new file mode 100644
index 0000000..bc6bd3a
--- /dev/null
+++ b/.cursor/rules/ui/RULE.md
@@ -0,0 +1,29 @@
+---
+description: Standards for building LitElement UI components in src/ui/
+globs: src/ui/**
+alwaysApply: false
+---
+
+# **UI Component Standards**
+
+## **Framework**
+
+- Use **LitElement** for all UI components.
+- Styles must be scoped within static get styles().
+
+## **Integration Logic**
+
+1. **Event-Driven:** UI components must **never** reference the GameLoop or GameStateManager directly for write operations.
+ - _Wrong:_ gameLoop.startLevel()
+ - _Right:_ this.dispatchEvent(new CustomEvent('start-level', ...))
+2. **Data Binding:** Components should accept data via **Properties** (static get properties()) or **Subscriptions** to Singleton EventTargets (e.g., gameStateManager).
+3. **Lazy Loading:** Large UI screens (TeamBuilder, MissionBoard) must be designed to be dynamically imported only when needed.
+
+## **Visuals**
+
+- **Aesthetic:** "Voxel-Web" / High-Tech Fantasy.
+ - Fonts: Monospace ('Courier New').
+ - Colors: Dark backgrounds (rgba(0,0,0,0.8)), Neon accents (Cyan, Gold, Green).
+ - Elements: Borders should look like pixel art or voxel edges.
+- **Responsive:** All screens must support Mobile (vertical stack) and Desktop (horizontal layout). Use CSS Grid/Flexbox.
+- **Accessibility:** Interactive elements must be \