feat: Establish comprehensive project rules and add initial game assets including 3D models and item data.
This commit is contained in:
parent
bf40a0f788
commit
051c47ef07
18 changed files with 1882 additions and 0 deletions
41
.agent/rules/RULE.md
Normal file
41
.agent/rules/RULE.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
description: High-level technical standards, file structure, and testing requirements for the Aether Shards project.
|
||||||
|
globs: src/*.js, test/*.js**
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# **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).
|
||||||
|
- Lazy load at need, only staticly import if something is needed at load, prior to user interaction.
|
||||||
|
- 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.
|
||||||
162
.agent/rules/core/CombatIntegration/RULE.md
Normal file
162
.agent/rules/core/CombatIntegration/RULE.md
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
31
.agent/rules/core/RULE.md
Normal file
31
.agent/rules/core/RULE.md
Normal file
|
|
@ -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.
|
||||||
190
.agent/rules/data/Inventory/RULE.md
Normal file
190
.agent/rules/data/Inventory/RULE.md
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
---
|
||||||
|
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`
|
||||||
|
|
||||||
|
|
||||||
23
.agent/rules/data/RULE.md
Normal file
23
.agent/rules/data/RULE.md
Normal file
|
|
@ -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.
|
||||||
22
.agent/rules/generation/RULE.md
Normal file
22
.agent/rules/generation/RULE.md
Normal file
|
|
@ -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.
|
||||||
156
.agent/rules/logic/CombatSkillUsage/RULE.md
Normal file
156
.agent/rules/logic/CombatSkillUsage/RULE.md
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
102
.agent/rules/logic/CombatState/RULE.md
Normal file
102
.agent/rules/logic/CombatState/RULE.md
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
175
.agent/rules/logic/EffectProcessor/RULE.md
Normal file
175
.agent/rules/logic/EffectProcessor/RULE.md
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
---
|
||||||
|
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()`
|
||||||
|
|
||||||
|
|
||||||
220
.agent/rules/logic/Marketplace/RULE.md
Normal file
220
.agent/rules/logic/Marketplace/RULE.md
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
---
|
||||||
|
description: Marketplace system architecture - the Gilded Bazaar for buying and selling items in the Hub
|
||||||
|
globs: src/managers/MarketManager.js, src/ui/screens/MarketplaceScreen.js, src/core/Persistence.js
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# **Marketplace System Rule**
|
||||||
|
|
||||||
|
The Marketplace (The Gilded Bazaar) is the primary gold sink and progression accelerator in the Hub. It allows players to purchase items with currency earned from missions and sell items for buyback.
|
||||||
|
|
||||||
|
## **1. System Architecture**
|
||||||
|
|
||||||
|
The Marketplace is managed by **MarketManager**, a singleton logic controller instantiated by GameStateManager.
|
||||||
|
|
||||||
|
### **Integration Flow**
|
||||||
|
|
||||||
|
1. **Mission Complete:** MissionManager dispatches `mission-victory` event. MarketManager listens and sets `needsRefresh` flag.
|
||||||
|
2. **Hub Entry:** Player enters Hub. MarketManager checks `needsRefresh` flag. If true, generates new stock based on tier and saves immediately.
|
||||||
|
3. **UI Render:** HubScreen passes the MarketManager instance to the `<marketplace-screen>` component.
|
||||||
|
|
||||||
|
### **Persistence**
|
||||||
|
|
||||||
|
- Market state (stock, buyback queue) is saved to IndexedDB (`market_state` store) to prevent players from reloading to re-roll shop inventory.
|
||||||
|
- Stock generation is deterministic based on tier and generation timestamp.
|
||||||
|
- Each transaction (buy/sell) immediately persists state to prevent duplication or currency desync.
|
||||||
|
|
||||||
|
## **2. Data Model**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/managers/MarketManager.js (JSDoc types)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} MarketItem
|
||||||
|
* @property {string} id - Unique Stock ID (e.g. "STOCK_001")
|
||||||
|
* @property {string} defId - Reference to ItemRegistry (e.g. "ITEM_RUSTY_BLADE")
|
||||||
|
* @property {string} type - ItemType (cached for filtering)
|
||||||
|
* @property {string} rarity - Rarity (cached for sorting/styling)
|
||||||
|
* @property {number} price - Purchase price
|
||||||
|
* @property {number} discount - 0.0 to 1.0 (percent off)
|
||||||
|
* @property {boolean} purchased - If true, show as "Sold Out"
|
||||||
|
* @property {Object} [instanceData] - If this is a buyback, store the ItemInstance here
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} MarketState
|
||||||
|
* @property {string} generationId - Timestamp or Mission Count when this stock was generated
|
||||||
|
* @property {MarketItem[]} stock - The active inventory for sale
|
||||||
|
* @property {MarketItem[]} buyback - Items sold by the player this session (can be bought back)
|
||||||
|
* @property {string} [specialOffer] - ID of a specific item (Daily Deal)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} StockTable
|
||||||
|
* @property {number} minItems - Minimum items to generate
|
||||||
|
* @property {number} maxItems - Maximum items to generate
|
||||||
|
* @property {Object} rarityWeights - Rarity distribution weights
|
||||||
|
* @property {string[]} allowedTypes - Item types this merchant can sell
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
## **3. Logic & Algorithms**
|
||||||
|
|
||||||
|
### **A. Stock Generation (`generateStock(tier)`)**
|
||||||
|
|
||||||
|
Triggered only when a run is completed and player enters Hub.
|
||||||
|
|
||||||
|
**Tier 1 (Early Game - Before Mission 3):**
|
||||||
|
|
||||||
|
- **Smith:\*\*** 5 Common Weapons, 3 Common Armor
|
||||||
|
- **Alchemist:** 5 Potions (Stacked), 2 Grenades (when consumables are available)
|
||||||
|
|
||||||
|
**Tier 2 (Mid Game - After Mission 3):**
|
||||||
|
|
||||||
|
- **Weights:** Common (60%), Uncommon (30%), Rare (10%), Ancient (0%)
|
||||||
|
- **Scavenger:** Unlocks. Sells 3 "Mystery Box" items (Unidentified Relics) - Future feature
|
||||||
|
|
||||||
|
**Algorithm:**
|
||||||
|
|
||||||
|
1. Filter `ItemRegistry` by `allowedTypes` and current tier availability
|
||||||
|
2. Roll `RNG` against `rarityWeights` to determine rarity
|
||||||
|
3. Select random Item ID from filtered pool matching selected rarity
|
||||||
|
4. Calculate Price: `BaseValue * (1 + RandomVariance(±10%))`
|
||||||
|
5. Create `MarketItem` with unique stock ID
|
||||||
|
6. Save state to IndexedDB immediately
|
||||||
|
|
||||||
|
### **B. Transaction Processing**
|
||||||
|
|
||||||
|
Transactions must be **atomic** to prevent item duplication or currency desync.
|
||||||
|
|
||||||
|
**Buy Logic (`buyItem(stockId)`):**
|
||||||
|
|
||||||
|
1. Check `Wallet >= Price` (validate currency)
|
||||||
|
2. Deduct Currency from `hubStash.currency.aetherShards`
|
||||||
|
3. **Generate Instance:** Create a new `ItemInstance` with a fresh UID (`ITEM_{TIMESTAMP}_{RANDOM}`)
|
||||||
|
4. Add to `InventoryManager.hubStash`
|
||||||
|
5. Mark `MarketItem` as `purchased: true`
|
||||||
|
6. Save State to IndexedDB
|
||||||
|
7. Return `true` on success, `false` on failure
|
||||||
|
|
||||||
|
**Sell Logic (`sellItem(itemUid)`):**
|
||||||
|
|
||||||
|
1. Find `ItemInstance` in `hubStash` by UID
|
||||||
|
2. Remove `ItemInstance` from `hubStash`
|
||||||
|
3. Calculate Value: `BasePrice * 0.25` (25% of base value)
|
||||||
|
4. Add Currency to `hubStash.currency.aetherShards`
|
||||||
|
5. **Create Buyback:** Convert instance to `MarketItem` and add to `buyback` array (Limit 10, oldest removed if full)
|
||||||
|
6. Save State to IndexedDB
|
||||||
|
7. Return `true` on success, `false` on failure
|
||||||
|
|
||||||
|
### **C. Merchant Types**
|
||||||
|
|
||||||
|
Merchants filter stock by item type:
|
||||||
|
|
||||||
|
- **SMITH:** `["WEAPON", "ARMOR"]`
|
||||||
|
- **TAILOR:** `["ARMOR"]`
|
||||||
|
- **ALCHEMIST:** `["CONSUMABLE", "UTILITY"]`
|
||||||
|
- **SCAVENGER:** `["RELIC", "UTILITY"]` (Tier 2+)
|
||||||
|
- **BUYBACK:** Shows all items in `buyback` array
|
||||||
|
|
||||||
|
## **4. UI Implementation (LitElement)**
|
||||||
|
|
||||||
|
**Component:** `src/ui/screens/MarketplaceScreen.js`
|
||||||
|
|
||||||
|
### **Visual Layout**
|
||||||
|
|
||||||
|
- **Grid Container:** CSS Grid `250px 1fr` (Sidebar | Main Content)
|
||||||
|
- **Sidebar (Merchants):** Vertical tabs with icons
|
||||||
|
- [⚔️ Smith]
|
||||||
|
- [🧥 Tailor]
|
||||||
|
- [⚗️ Alchemist]
|
||||||
|
- [♻️ Buyback]
|
||||||
|
- **Main Content:**
|
||||||
|
- **Filter Bar:** "All", "Weapons", "Armor", "Utility", "Consumables"
|
||||||
|
- **Item Grid:** Flex-wrap container of Item Cards with voxel-style borders
|
||||||
|
- **Modal:** Purchase confirmation dialog ("Buy for 50?")
|
||||||
|
|
||||||
|
### **Interactive States**
|
||||||
|
|
||||||
|
- **Affordable:** Price Text is Gold/Green, button enabled
|
||||||
|
- **Unaffordable:** Price Text is Red, button disabled
|
||||||
|
- **Sold Out:** Card is dimmed (opacity 0.5), overlay text "SOLD", cursor not-allowed
|
||||||
|
|
||||||
|
### **Rarity Styling**
|
||||||
|
|
||||||
|
Item cards use border colors to indicate rarity:
|
||||||
|
|
||||||
|
- **COMMON:** `#888` (Gray)
|
||||||
|
- **UNCOMMON:** `#00ff00` (Green)
|
||||||
|
- **RARE:** `#0088ff` (Blue)
|
||||||
|
- **ANCIENT:** `#ff00ff` (Magenta)
|
||||||
|
|
||||||
|
### **Events**
|
||||||
|
|
||||||
|
- **`market-closed`:** Dispatched when player clicks close button. HubScreen listens and closes overlay.
|
||||||
|
|
||||||
|
## **5. Integration Points**
|
||||||
|
|
||||||
|
### **A. GameStateManager**
|
||||||
|
|
||||||
|
- MarketManager is instantiated in GameStateManager constructor
|
||||||
|
- Requires: `persistence`, `itemRegistry`, `hubInventoryManager`, `missionManager`
|
||||||
|
- Initialized in `GameStateManager.init()` after persistence is ready
|
||||||
|
- `checkRefresh()` is called when transitioning to `STATE_MAIN_MENU`
|
||||||
|
|
||||||
|
### **B. HubScreen**
|
||||||
|
|
||||||
|
- When `activeOverlay === "MARKET"`, renders `<marketplace-screen>` component
|
||||||
|
- Passes `marketManager` instance as property
|
||||||
|
- Listens for `market-closed` event to close overlay
|
||||||
|
|
||||||
|
### **C. Persistence**
|
||||||
|
|
||||||
|
- Market state stored in IndexedDB `Market` store (version 3+)
|
||||||
|
- Key: `"market_state"`
|
||||||
|
- Saved after every transaction (buy/sell) and stock generation
|
||||||
|
|
||||||
|
## **6. Conditions of Acceptance (CoA)**
|
||||||
|
|
||||||
|
**CoA 1: Persistence Integrity**
|
||||||
|
|
||||||
|
- Buying an item, saving, and reloading the page must result in:
|
||||||
|
- Item being in `hubStash` inventory
|
||||||
|
- Shop showing "Sold Out" for that item
|
||||||
|
- Stock **must not change** upon reload (same `generationId`)
|
||||||
|
- Test: Buy item → Save → Reload → Verify item in stash and stock unchanged
|
||||||
|
|
||||||
|
**CoA 2: Currency Math**
|
||||||
|
|
||||||
|
- Buying an item costs exactly the listed price (no rounding errors)
|
||||||
|
- Selling an item refunds exactly `BasePrice * 0.25` (rounded down)
|
||||||
|
- Buyback allows repurchasing a sold item for the **exact amount it was sold for** (undo logic)
|
||||||
|
- Test: Buy for 50 → Wallet decreases by 50. Sell for 12 → Wallet increases by 12. Buyback costs 12.
|
||||||
|
|
||||||
|
**CoA 3: Atomic Transactions**
|
||||||
|
|
||||||
|
- If currency deduction fails, item should not be added to inventory
|
||||||
|
- If item removal fails, currency should not be added
|
||||||
|
- State must be consistent after any transaction (success or failure)
|
||||||
|
- Test: Attempt buy with insufficient funds → Verify no item added, currency unchanged
|
||||||
|
|
||||||
|
**CoA 4: Stock Generation**
|
||||||
|
|
||||||
|
- Tier 1 stock must contain only Common items
|
||||||
|
- Tier 2 stock must follow rarity weights (60% Common, 30% Uncommon, 10% Rare)
|
||||||
|
- Stock must be generated only when `needsRefresh` is true and player enters Hub
|
||||||
|
- Test: Complete mission → Enter Hub → Verify new stock generated with correct tier distribution
|
||||||
|
|
||||||
|
**CoA 5: Buyback Limit**
|
||||||
|
|
||||||
|
- Buyback array must not exceed 10 items
|
||||||
|
- When adding 11th item, oldest item must be removed
|
||||||
|
- Test: Sell 11 items → Verify buyback contains only last 10 items
|
||||||
|
|
||||||
|
## **7. Future Enhancements (Optional)**
|
||||||
|
|
||||||
|
- **Class Filtering:** Visually flag items that cannot be equipped by anyone in current Roster
|
||||||
|
- **Daily Deals:** Special offers with discounts on specific items
|
||||||
|
- **Scavenger Merchant:** Sells unidentified relics (Mystery Boxes) that must be identified
|
||||||
|
- **Price Negotiation:** Skill-based haggling system (future feature)
|
||||||
|
|
||||||
24
.agent/rules/logic/RULE.md
Normal file
24
.agent/rules/logic/RULE.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
description: Standards for gameplay logic, AI, and Effect processing.
|
||||||
|
globs: src/systems/*.js, src/managers/*.js**
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# **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.
|
||||||
116
.agent/rules/logic/TurnLifecycle/RULE.md
Normal file
116
.agent/rules/logic/TurnLifecycle/RULE.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
78
.agent/rules/logic/TurnSystem/RULE.md
Normal file
78
.agent/rules/logic/TurnSystem/RULE.md
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
117
.agent/rules/ui/CharacterSheet/RULE.md
Normal file
117
.agent/rules/ui/CharacterSheet/RULE.md
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
---
|
||||||
|
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 `<skill-tree-ui .unit=${this.unit}></skill-tree-ui>`
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
141
.agent/rules/ui/CombatHUD/RULE.md
Normal file
141
.agent/rules/ui/CombatHUD/RULE.md
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
---
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
138
.agent/rules/ui/HubScreen/RULE.md
Normal file
138
.agent/rules/ui/HubScreen/RULE.md
Normal file
|
|
@ -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 `<hub-screen>`
|
||||||
|
2. **Mission Selection:**
|
||||||
|
- User clicks "Mission Board"
|
||||||
|
- `<hub-screen>` mounts `<mission-board>` overlay
|
||||||
|
- `<mission-board>` emits `mission-selected`
|
||||||
|
3. **Squad Assembly:**
|
||||||
|
- `<hub-screen>` 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 `<mission-board>` 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
|
||||||
31
.agent/rules/ui/RULE.md
Normal file
31
.agent/rules/ui/RULE.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
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.
|
||||||
|
- Filename should match the component name (kebab-case)
|
||||||
|
- Styles must be scoped within static get styles().
|
||||||
|
- Use theme styles where applicable
|
||||||
|
|
||||||
|
## **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 \<button\>. Use aria-label for icon-only buttons.
|
||||||
115
.agent/rules/ui/SkillTree/RULE.md
Normal file
115
.agent/rules/ui/SkillTree/RULE.md
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
---
|
||||||
|
description: Skill Tree UI component - interactive progression tree for Explorer units
|
||||||
|
globs: src/ui/components/SkillTreeUI.js, src/ui/components/SkillTreeUI.ts, src/types/SkillTreeUI.ts
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# **Skill Tree UI Rule**
|
||||||
|
|
||||||
|
This rule defines the technical implementation for the SkillTreeUI component. This component renders the interactive progression tree for a specific Explorer.
|
||||||
|
|
||||||
|
## **1. Visual Architecture**
|
||||||
|
|
||||||
|
**Style:** "Voxel-Web". We will use **CSS 3D Transforms** to render the nodes as rotating cubes, keeping the UI lightweight but consistent with the game's aesthetic.
|
||||||
|
|
||||||
|
### **A. The Tree Container (Scroll View)**
|
||||||
|
|
||||||
|
- **Layout:** A vertical flex container
|
||||||
|
- **Tiers:** Each "Rank" (Novice, Apprentice, etc.) is a horizontal row (Flexbox)
|
||||||
|
- **Connections:** An `<svg>` overlay sits behind the nodes to draw connecting lines (cables)
|
||||||
|
|
||||||
|
### **B. The Node (CSS Voxel)**
|
||||||
|
|
||||||
|
- **Structure:** A div with `preserve-3d` containing 6 faces
|
||||||
|
- **Animation:**
|
||||||
|
- _Locked:_ Static grey cube
|
||||||
|
- _Available:_ Slowly bobbing, pulsing color
|
||||||
|
- _Unlocked:_ Rotating slowly, emitting a glow (box-shadow)
|
||||||
|
- **Content:** An icon (`<img>` or FontAwesome) is mapped to the Front face
|
||||||
|
|
||||||
|
### **C. The Inspector (Footer)**
|
||||||
|
|
||||||
|
- A slide-up panel showing details for the _selected_ node
|
||||||
|
- Contains the "Unlock" button
|
||||||
|
|
||||||
|
## **2. TypeScript Interfaces (Data Model)**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/types/SkillTreeUI.ts
|
||||||
|
|
||||||
|
export interface SkillTreeProps {
|
||||||
|
/** The Unit object (source of state) */
|
||||||
|
unit: Explorer;
|
||||||
|
/** The Tree Definition (source of layout) */
|
||||||
|
treeDef: SkillTreeDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillNodeState {
|
||||||
|
id: string;
|
||||||
|
def: SkillNodeDefinition;
|
||||||
|
status: 'LOCKED' | 'AVAILABLE' | 'UNLOCKED';
|
||||||
|
/** Calculated position for drawing lines */
|
||||||
|
domRect?: DOMRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillTreeEvents {
|
||||||
|
/** Dispatched when user attempts to spend SP */
|
||||||
|
'unlock-request': {
|
||||||
|
nodeId: string;
|
||||||
|
cost: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## **3. Interaction Logic**
|
||||||
|
|
||||||
|
### **A. Node Status Calculation**
|
||||||
|
|
||||||
|
The UI must determine the state of every node on render:
|
||||||
|
|
||||||
|
1. **UNLOCKED:** `unit.classMastery.unlockedNodes.includes(node.id)`
|
||||||
|
2. **AVAILABLE:** Not unlocked AND `parent` is Unlocked AND `unit.level >= node.req`
|
||||||
|
3. **LOCKED:** Everything else
|
||||||
|
|
||||||
|
### **B. Connection Drawing**
|
||||||
|
|
||||||
|
Since nodes are DOM elements, we need a `ResizeObserver` to track their positions.
|
||||||
|
|
||||||
|
- **Logic:** Calculate center `(x, y)` of Parent Node and Child Node relative to the Container
|
||||||
|
- **Drawing:** Draw a `<path>` or `<line>` in the SVG layer with a "Circuit Board" style (90-degree bends)
|
||||||
|
- **Styling:**
|
||||||
|
- If Child is Unlocked: Line is **Bright Blue/Gold** (Neon)
|
||||||
|
- If Child is Available: Line is **Dim**
|
||||||
|
- If Child is Locked: Line is **Dark Grey**
|
||||||
|
|
||||||
|
## **4. Conditions of Acceptance (CoA)**
|
||||||
|
|
||||||
|
**CoA 1: Dynamic Rendering**
|
||||||
|
|
||||||
|
- The Tree must handle variable depths (Tier 1 to Tier 5)
|
||||||
|
- Nodes must visibly update state immediately when `unit` prop changes (e.g., after unlocking)
|
||||||
|
|
||||||
|
**CoA 2: Validation Feedback**
|
||||||
|
|
||||||
|
- Clicking a "LOCKED" node should show the inspector but disable the button with a reason (e.g., "Requires: Shield Bash")
|
||||||
|
- Clicking an "AVAILABLE" node with 0 SP should show "Insufficient Points"
|
||||||
|
|
||||||
|
**CoA 3: Responsive Lines**
|
||||||
|
|
||||||
|
- If the window resizes, the SVG connecting lines must redraw to connect the centers of the cubes accurately
|
||||||
|
|
||||||
|
**CoA 4: Scroll Position**
|
||||||
|
|
||||||
|
- On open, the view should automatically scroll to center on the _highest tier_ that has an "Available" node, so the player sees their next step
|
||||||
|
|
||||||
|
## **5. Implementation Requirements**
|
||||||
|
|
||||||
|
Create `src/ui/components/SkillTreeUI.js` as a LitElement:
|
||||||
|
|
||||||
|
1. **CSS 3D:** Implement a `.voxel-node` class using `transform-style: preserve-3d` to create a cube. Use keyframes for rotation
|
||||||
|
2. **Layout:** Render the tree tiers using `flex-direction: column-reverse` (Tier 1 at bottom)
|
||||||
|
3. **SVG Lines:** Implement a `_updateConnections()` method that uses `getBoundingClientRect()` to draw lines between nodes in an absolute-positioned `<svg>`. Call this on resize and first render
|
||||||
|
4. **Interactivity:** Clicking a node selects it. Show details in a fixed footer
|
||||||
|
5. **Logic:** Calculate `LOCKED/AVAILABLE/UNLOCKED` state based on `this.unit.unlockedNodes`
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in a new issue