fix: bugs #238,#240,#244,#246 + docs #248 update
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
- #238: Fix spire tab inconsistent state (Max Floor 1 but Floors Cleared 0) by not inflating maxFloorReached on enterSpireMode and preserving it on exitSpireMode - #240: Fix guardian armor display stray text by extracting stat formatters in SpireSummaryTab - #244: Improve discipline auto-pause UX with log messages and visual feedback on DisciplineCard - #246: Fix raw mana exceeding max cap by recomputing maxMana after discipline XP gains - #248: Update AGENTS.md (remove gitea_get_project_boards, add gitea_start_session, 22 mana types, 8 stores, updated guardian tiers) - #248: Update README.md (remove Prisma/SQLite refs, update mana types/guardian tiers/discipline counts) - #248: Update GAME_BRIEFING.md (8 stores, 22 mana types, 64 disciplines, 8-tier guardians, correct code architecture)
This commit is contained in:
@@ -24,13 +24,14 @@ git add -A && git commit -m "type: desc" && git push origin master
|
|||||||
|
|
||||||
1. `docs/project-structure.txt`
|
1. `docs/project-structure.txt`
|
||||||
2. `docs/dependency-graph.json`
|
2. `docs/dependency-graph.json`
|
||||||
3. `gitea_get_project_boards` → resume in-progress or pick top todo
|
3. `gitea_start_session` → retrieve active task registry and issues
|
||||||
4. `gitea_update_issue_status` → `ai_state: "in-progress"`
|
4. Evaluate the queue to find the highest-priority `ai_state: todo` item (or locate an existing `in-progress` task if resuming work)
|
||||||
5. Work, log with `gitea_add_comment`, then `gitea_update_issue_status` → `ai_state: "done"`
|
5. `gitea_update_issue_status` → `ai_state: "in-progress"`
|
||||||
|
6. Work, log with `gitea_add_comment`, then `gitea_update_issue_status` → `ai_state: "done"`
|
||||||
|
|
||||||
## Labels
|
## Labels
|
||||||
|
|
||||||
`ai_state: todo` | `in_state: in-progress` | `ai_state: review` | `ai_state: blocked` | `ai_state: done`
|
`ai_state: todo` | `ai_state: in-progress` | `ai_state: review` | `ai_state: blocked` | `ai_state: done`
|
||||||
|
|
||||||
## Terminal Tool
|
## Terminal Tool
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ Use for 3+ sequential independent calls. Zero context from parent — paste ever
|
|||||||
|
|
||||||
- **Stack:** Next.js 16, TS 5, Tailwind 4 + shadcn/ui, Zustand+persist, Vitest, Bun
|
- **Stack:** Next.js 16, TS 5, Tailwind 4 + shadcn/ui, Zustand+persist, Vitest, Bun
|
||||||
- **No backend:** Pure client-side. No Prisma, no database. State persisted to localStorage.
|
- **No backend:** Pure client-side. No Prisma, no database. State persisted to localStorage.
|
||||||
- **Active stores (7 Zustand stores):**
|
- **Active stores (8 Zustand stores):**
|
||||||
- `useGameStore` — Coordinator/tick pipeline, imports all other stores
|
- `useGameStore` — Coordinator/tick pipeline, imports all other stores
|
||||||
- `useManaStore` — Mana pools, regen, element conversion
|
- `useManaStore` — Mana pools, regen, element conversion
|
||||||
- `useCombatStore` — Spire/floors, combat, spells, achievements
|
- `useCombatStore` — Spire/floors, combat, spells, achievements
|
||||||
@@ -120,9 +121,9 @@ ManaDrainPerTick = drainBase × (1 + (XP / difficultyFactor)^0.4)
|
|||||||
### Guardian System
|
### Guardian System
|
||||||
- Guardians on every 10th floor
|
- Guardians on every 10th floor
|
||||||
- **Base (floors 10–80):** 7 base elements + Transference, static definitions with unique names
|
- **Base (floors 10–80):** 7 base elements + Transference, static definitions with unique names
|
||||||
- **Compound (floors 90–110):** Metal, Sand, Lightning — procedurally named
|
- **Tier 2 — Composite (floors 90–160):** 8 composite elements (Metal, Sand, Lightning, Frost, BlackFlame, RadiantFlames, Miasma, ShadowGlass)
|
||||||
- **Exotic (floors 120–140):** Crystal, Stellar, Void — procedurally named
|
- **Tier 3 — Exotic (floors 170–240):** 6 exotic elements (Crystal, Stellar, Void, Soul, Time, Plasma)
|
||||||
- **Combination bosses (floor 150+):** Dual-element procedural guardians cycling through 9 element pairs, scaling indefinitely
|
- **Tier 4+ — Procedural (floors 250+):** Dual-element → multi-element combination bosses cycling through element pairs, scaling indefinitely through 8 tiers
|
||||||
- HP formula: `floor(5000 × (floor/10) ^ (1.1 + floor/200))`
|
- HP formula: `floor(5000 × (floor/10) ^ (1.1 + floor/200))`
|
||||||
- Pact signing: costs raw mana + time, grants permanent boons
|
- Pact signing: costs raw mana + time, grants permanent boons
|
||||||
|
|
||||||
@@ -165,3 +166,5 @@ Lifesteal/healing, scroll crafting, ascension skills, LabTab, pause mechanics, f
|
|||||||
**Utility (1):** Transference 🔗
|
**Utility (1):** Transference 🔗
|
||||||
**Composite (8):** Fire+Earth=Metal ⚙️, Earth+Water=Sand ⏳, Fire+Air=Lightning ⚡, Air+Water=Frost ❄️, Dark+Fire=BlackFlame 🌋, Light+Fire=Radiant Flames 🌟, Air+Death=Miasma ☁️, Earth+Dark=Shadow Glass 🖤
|
**Composite (8):** Fire+Earth=Metal ⚙️, Earth+Water=Sand ⏳, Fire+Air=Lightning ⚡, Air+Water=Frost ❄️, Dark+Fire=BlackFlame 🌋, Light+Fire=Radiant Flames 🌟, Air+Death=Miasma ☁️, Earth+Dark=Shadow Glass 🖤
|
||||||
**Exotic (6):** Sand+Sand+Light=Crystal 💎, Plasma+Light+Fire=Stellar ⭐, Dark+Dark+Death=Void 🕳️, Light+Dark+Transference=Soul 💫, Soul+Sand+Transference=Time ⏱️, Lightning+Fire+Transference=Plasma ⚡
|
**Exotic (6):** Sand+Sand+Light=Crystal 💎, Plasma+Light+Fire=Stellar ⭐, Dark+Dark+Death=Void 🕳️, Light+Dark+Transference=Soul 💫, Soul+Sand+Transference=Time ⏱️, Lightning+Fire+Transference=Plasma ⚡
|
||||||
|
|
||||||
|
**Total: 22 mana types** (7 base + 1 utility + 8 composite + 6 exotic)
|
||||||
|
|||||||
@@ -46,51 +46,57 @@
|
|||||||
|
|
||||||
### Core Game Loop
|
### Core Game Loop
|
||||||
|
|
||||||
1. **Gather Mana** - Click to collect mana or let it regenerate automatically (14 total mana types)
|
1. **Gather Mana** — Click to collect mana or let it regenerate automatically (22 total mana types)
|
||||||
2. **Practice Disciplines** - Continuously train abilities that drain mana each tick in exchange for growing stat bonuses
|
2. **Practice Disciplines** — Continuously train abilities that drain mana each tick in exchange for growing stat bonuses
|
||||||
3. **Climb the Spire** - Battle through procedurally-generated floors; every 10th floor is a guardian encounter
|
3. **Climb the Spire** — Battle through procedurally-generated floors; every 10th floor is a guardian encounter
|
||||||
4. **Craft & Enchant** - 3-stage equipment enchantment system with capacity limits
|
4. **Craft & Enchant** — 3-stage equipment enchantment system with capacity limits
|
||||||
5. **Summon Golems** - Magical constructs that fight alongside you (4 base + 6 hybrid types)
|
5. **Summon Golems** — Magical constructs that fight alongside you (1 base + 3 elemental + 6 hybrid types)
|
||||||
6. **Prestige (Loop)** - Reset progress for Insight currency, gain permanent bonuses
|
6. **Prestige (Loop)** — Reset progress for Insight currency, gain permanent bonuses
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🔮 Mana System
|
### 🔮 Mana System
|
||||||
- **14 Mana Types**: 7 base elements + 1 utility + 3 compound + 3 exotic
|
|
||||||
|
- **22 Mana Types**: 7 base elements + 1 utility + 8 composite + 6 exotic
|
||||||
- Elemental conversion, regeneration mechanics, and meditation bonuses
|
- Elemental conversion, regeneration mechanics, and meditation bonuses
|
||||||
- Mana types: Fire, Water, Air, Earth, Light, Dark, Death (base), Transference (utility), Metal, Sand, Lightning (compound), Crystal, Stellar, Void (exotic)
|
- Mana types: Fire, Water, Air, Earth, Light, Dark, Death (base), Transference (utility), Metal, Sand, Lightning, Frost, BlackFlame, RadiantFlames, Miasma, ShadowGlass (composite), Crystal, Stellar, Void, Soul, Time, Plasma (exotic)
|
||||||
|
|
||||||
### 📜 Discipline System
|
### 📜 Discipline System
|
||||||
- Practice-based progression - no discrete levels, only continuous XP growth
|
|
||||||
|
- Practice-based progression — no discrete levels, only continuous XP growth
|
||||||
- Disciplines drain mana each tick; stat bonuses grow as a power curve of accumulated XP
|
- Disciplines drain mana each tick; stat bonuses grow as a power curve of accumulated XP
|
||||||
- Perks unlock at XP thresholds (once, capped, or infinite stacking)
|
- Perks unlock at XP thresholds (once, capped, or infinite stacking)
|
||||||
- Attunement-gated discipline pools (Base / Enchanter / Invoker / Fabricator)
|
- Attunement-gated discipline pools (Base / Elemental / Enchanter / Invoker / Fabricator)
|
||||||
- Concurrent discipline slots unlock as total XP grows (max 4)
|
- Concurrent discipline slots unlock as total XP grows (max 4)
|
||||||
|
|
||||||
### ⚔️ Combat & Spire
|
### ⚔️ Combat & Spire
|
||||||
|
|
||||||
- Cast-speed based combat system with elemental effectiveness
|
- Cast-speed based combat system with elemental effectiveness
|
||||||
- Multi-spell support from equipped weapons
|
- Multi-spell support from equipped weapons
|
||||||
- Every 10th floor is a guardian: base elements (10–80), compound (90–110), exotic (120–140), then procedural combination bosses (150+)
|
- Every 10th floor is a guardian: base elements (10–80), composite (90–160), exotic (170–240), then procedural combination bosses (250+)
|
||||||
- Golem allies that deal automatic damage each tick
|
- Golem allies that deal automatic damage each tick
|
||||||
- Enemy modifiers: Armored, Agile, Mage, Shield, Swarm
|
- Enemy modifiers: Armored, Agile, Mage, Shield, Swarm
|
||||||
|
|
||||||
### 🛡️ Equipment & Enchanting
|
### 🛡️ Equipment & Enchanting
|
||||||
|
|
||||||
- 3-stage enchantment process: Design → Prepare → Apply
|
- 3-stage enchantment process: Design → Prepare → Apply
|
||||||
- Equipment capacity system limiting total enchantment power
|
- Equipment capacity system limiting total enchantment power
|
||||||
- Enchantment effects: stat bonuses, multipliers, spell grants
|
- Enchantment effects: stat bonuses, multipliers, spell grants
|
||||||
- Disenchanting to recover mana (only in Prepare stage)
|
- Disenchanting to recover mana (only in Prepare stage)
|
||||||
- Weapon/armor slots with 2-handed weapon support
|
- 8 equipment slots with 50 equipment types across 9 categories
|
||||||
|
|
||||||
### 🤖 Golemancy System
|
### 🤖 Golemancy System
|
||||||
- Summon magical constructs (Earth, Steel, Crystal, Sand + 6 hybrid types)
|
|
||||||
|
- 10 golems total: 1 base (Earth) + 3 elemental (Steel, Crystal, Sand) + 6 hybrid types
|
||||||
- Golem slots unlock every 2 Fabricator levels (max 5 slots at Level 10)
|
- Golem slots unlock every 2 Fabricator levels (max 5 slots at Level 10)
|
||||||
- Hybrid golems require Enchanter 5 + Fabricator 5
|
- Hybrid golems require Enchanter 5 + Fabricator 5
|
||||||
|
|
||||||
### 🔄 Prestige (Insight)
|
### 🔄 Prestige (Insight)
|
||||||
|
|
||||||
- Reset progress for permanent Insight currency
|
- Reset progress for permanent Insight currency
|
||||||
- Insight upgrades across multiple categories
|
- Insight upgrades across 14 categories
|
||||||
- Signed pacts and attunements persist through prestige
|
- Signed pacts and attunements persist through prestige
|
||||||
- Three attunement classes: Enchanter (Transference), Invoker (Spells/Pacts), Fabricator (Golems/Equipment)
|
- Three attunement classes: Enchanter (Transference), Invoker (Spells/Pacts), Fabricator (Golems/Equipment)
|
||||||
|
|
||||||
@@ -99,27 +105,24 @@
|
|||||||
## Tech Stack
|
## Tech Stack
|
||||||
|
|
||||||
| Technology | Version | Purpose |
|
| Technology | Version | Purpose |
|
||||||
|------------|---------|---------|
|
|------------|---------|---------|
|
||||||
| **Next.js** | ^16.1.1 | Full-stack framework (App Router) |
|
| **Next.js** | ^16.1.1 | Full-stack framework (App Router) |
|
||||||
| **React** | ^19.0.0 | UI library |
|
| **React** | ^19.0.0 | UI library |
|
||||||
| **TypeScript** | ^5 | Type-safe development |
|
| **TypeScript** | ^5 | Type-safe development |
|
||||||
| **Tailwind CSS** | ^4 | Utility-first styling |
|
| **Tailwind CSS** | ^4 | Utility-first styling |
|
||||||
| **shadcn/ui** | Radix-based | Reusable UI components |
|
| **shadcn/ui** | Radix-based | Reusable UI components |
|
||||||
| **Zustand** | ^5.0.6 | Client state management (with persist) |
|
| **Zustand** | ^5.0.6 | Client state management (with persist) |
|
||||||
| **Prisma ORM** | ^6.11.1 | Database abstraction (SQLite) |
|
|
||||||
| **Bun** | Latest | JavaScript runtime & package manager |
|
| **Bun** | Latest | JavaScript runtime & package manager |
|
||||||
| **Vitest** | ^4.1.2 | Unit testing framework |
|
| **Vitest** | ^4.1.2 | Unit testing framework |
|
||||||
| **ESLint** | ^9 | Code linting |
|
| **ESLint** | ^9 | Code linting |
|
||||||
| **@tanstack/react-query** | ^5.82.0 | Data fetching/caching |
|
|
||||||
| **Framer Motion** | ^12.23.2 | Animation library |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- **Bun** runtime (recommended) or Node.js 18+
|
- **Bun** runtime (recommended) or Node.js 18+
|
||||||
- **SQLite** (for local development, included with Prisma)
|
|
||||||
- Git
|
- Git
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
@@ -134,11 +137,6 @@ bun install
|
|||||||
|
|
||||||
# Or using npm
|
# Or using npm
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Set up the database
|
|
||||||
bun run db:push
|
|
||||||
# or
|
|
||||||
npm run db:push
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
@@ -162,10 +160,6 @@ The game will be available at `http://localhost:3000`.
|
|||||||
| `lint` | Run ESLint |
|
| `lint` | Run ESLint |
|
||||||
| `test` | Run Vitest tests |
|
| `test` | Run Vitest tests |
|
||||||
| `test:coverage` | Run tests with coverage report |
|
| `test:coverage` | Run tests with coverage report |
|
||||||
| `db:push` | Push Prisma schema to database |
|
|
||||||
| `db:generate` | Generate Prisma client |
|
|
||||||
| `db:migrate` | Run database migrations |
|
|
||||||
| `db:reset` | Reset database |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -178,25 +172,17 @@ Mana-Loop/
|
|||||||
│ │ ├── layout.tsx # Root layout (metadata, fonts, providers)
|
│ │ ├── layout.tsx # Root layout (metadata, fonts, providers)
|
||||||
│ │ ├── page.tsx # Main game UI
|
│ │ ├── page.tsx # Main game UI
|
||||||
│ │ ├── globals.css # Global styles
|
│ │ ├── globals.css # Global styles
|
||||||
│ │ └── api/ # API routes (minimal)
|
│ │ └── components/ # App-level components
|
||||||
│ ├── components/ # React components
|
│ ├── components/ # React components
|
||||||
│ │ ├── ui/ # shadcn/ui components (20+ components)
|
│ │ ├── ui/ # shadcn/ui components (20+ components)
|
||||||
│ │ └── game/ # Game-specific components
|
│ │ └── game/ # Game-specific components
|
||||||
│ │ ├── tabs/ # Tab components (SpireTab, DisciplinesTab, etc.)
|
│ │ ├── tabs/ # Tab components (SpireTab, DisciplinesTab, etc.)
|
||||||
│ │ ├── ManaDisplay.tsx, ActionButtons.tsx, TimeDisplay.tsx
|
│ │ ├── ManaDisplay.tsx, ActionButtons.tsx, TimeDisplay.tsx
|
||||||
│ │ └── crafting/, debug/, shared/, stats/ subdirectories
|
│ │ └── crafting/, debug/, LootInventory/ subdirectories
|
||||||
│ ├── hooks/ # Custom React hooks (use-mobile, use-toast)
|
│ ├── hooks/ # Custom React hooks (use-mobile, use-toast)
|
||||||
│ └── lib/ # Utility libraries
|
│ └── lib/ # Utility libraries
|
||||||
│ └── game/ # Core game logic
|
│ └── game/ # Core game logic
|
||||||
│ ├── stores/ # Modular Zustand stores
|
│ ├── stores/ # 8 Modular Zustand stores (+ supporting files)
|
||||||
│ │ ├── gameStore.ts # Core state & tick logic
|
|
||||||
│ │ ├── manaStore.ts # Mana gathering & conversion
|
|
||||||
│ │ ├── combatStore.ts # Combat, spells, floor progression
|
|
||||||
│ │ ├── prestigeStore.ts # Prestige/loop & insight
|
|
||||||
│ │ ├── discipline-slice.ts # Discipline activation & XP
|
|
||||||
│ │ ├── attunementStore.ts # Attunement classes
|
|
||||||
│ │ ├── craftingStore.ts # Crafting state
|
|
||||||
│ │ └── uiStore.ts # UI state & modals
|
|
||||||
│ ├── crafting-actions/ # Modular crafting stage handlers
|
│ ├── crafting-actions/ # Modular crafting stage handlers
|
||||||
│ ├── constants/ # Elements, spells, rooms, prestige
|
│ ├── constants/ # Elements, spells, rooms, prestige
|
||||||
│ ├── data/ # Game data
|
│ ├── data/ # Game data
|
||||||
@@ -204,13 +190,11 @@ Mana-Loop/
|
|||||||
│ │ ├── enchantments/ # Enchantment effects by category
|
│ │ ├── enchantments/ # Enchantment effects by category
|
||||||
│ │ ├── equipment/ # Equipment type definitions
|
│ │ ├── equipment/ # Equipment type definitions
|
||||||
│ │ ├── golems/ # Golem definitions
|
│ │ ├── golems/ # Golem definitions
|
||||||
│ │ ├── guardian-data.ts # Static guardian definitions (floors 10–140)
|
│ │ ├── guardian-data.ts # Static guardian definitions (floors 10–240)
|
||||||
│ │ └── guardian-encounters.ts # Procedural guardian lookup & combo bosses
|
│ │ └── guardian-encounters.ts # Procedural guardian lookup & combo bosses (250+)
|
||||||
│ ├── effects/ # Unified stat computation
|
│ ├── effects/ # Unified stat computation
|
||||||
│ │ └── discipline-effects.ts # Discipline → getUnifiedEffects()
|
|
||||||
│ ├── types/ # TypeScript types (disciplines, elements, etc.)
|
│ ├── types/ # TypeScript types (disciplines, elements, etc.)
|
||||||
│ └── utils/ # Combat, floor, enemy, discipline math helpers
|
│ └── utils/ # Combat, floor, enemy, discipline math helpers
|
||||||
├── prisma/ # Database schema and migrations
|
|
||||||
├── public/ # Static assets
|
├── public/ # Static assets
|
||||||
├── docs/ # Project documentation
|
├── docs/ # Project documentation
|
||||||
│ ├── AGENTS.md # Architecture guide for AI agents
|
│ ├── AGENTS.md # Architecture guide for AI agents
|
||||||
@@ -229,16 +213,19 @@ For detailed architecture patterns and coding guidelines, see [AGENTS.md](./AGEN
|
|||||||
## Game Systems
|
## Game Systems
|
||||||
|
|
||||||
### Mana System
|
### Mana System
|
||||||
The core resource of the game with 14 distinct types organized in a hierarchy:
|
|
||||||
|
The core resource of the game with 22 distinct types organized in a hierarchy:
|
||||||
|
|
||||||
- **Base Elements (7)**: Fire, Water, Air, Earth, Light, Dark, Death
|
- **Base Elements (7)**: Fire, Water, Air, Earth, Light, Dark, Death
|
||||||
- **Utility (1)**: Transference (Enchanter attunement)
|
- **Utility (1)**: Transference (Enchanter attunement)
|
||||||
- **Compound (3)**: Metal (Fire+Earth), Sand (Earth+Water), Lightning (Fire+Air)
|
- **Composite (8)**: Metal (Fire+Earth), Sand (Earth+Water), Lightning (Fire+Air), Frost (Air+Water), BlackFlame (Dark+Fire), RadiantFlames (Light+Fire), Miasma (Air+Death), ShadowGlass (Earth+Dark)
|
||||||
- **Exotic (3)**: Crystal (Sand+Sand+Light), Stellar (Fire+Fire+Light), Void (Dark+Dark+Death)
|
- **Exotic (6)**: Crystal (Sand+Sand+Light), Stellar (Plasma+Light+Fire), Void (Dark+Dark+Death), Soul (Light+Dark+Transference), Time (Soul+Sand+Transference), Plasma (Lightning+Fire+Transference)
|
||||||
|
|
||||||
**Key Files**: `src/lib/game/stores/manaStore.ts`, `src/lib/game/constants/elements.ts`
|
**Key Files**: `src/lib/game/stores/manaStore.ts`, `src/lib/game/constants/elements.ts`
|
||||||
|
|
||||||
### Discipline System
|
### Discipline System
|
||||||
Disciplines replace the old skill system entirely. There are no discrete levels - disciplines grow **continuously** through practice. The player activates a discipline and it drains mana each tick in exchange for permanent stat growth within the run.
|
|
||||||
|
Disciplines replace the old skill system entirely. There are no discrete levels — disciplines grow **continuously** through practice. The player activates a discipline and it drains mana each tick in exchange for permanent stat growth within the run.
|
||||||
|
|
||||||
- **Stat bonus** grows as a power curve of XP: `baseValue × (XP / scalingFactor)^0.65`
|
- **Stat bonus** grows as a power curve of XP: `baseValue × (XP / scalingFactor)^0.65`
|
||||||
- **Mana drain** also increases with XP: `drainBase × (1 + (XP / difficultyFactor)^0.4)`
|
- **Mana drain** also increases with XP: `drainBase × (1 + (XP / difficultyFactor)^0.4)`
|
||||||
@@ -248,16 +235,18 @@ Disciplines replace the old skill system entirely. There are no discrete levels
|
|||||||
**Key Files**: `src/lib/game/data/disciplines/`, `src/lib/game/stores/discipline-slice.ts`, `src/lib/game/utils/discipline-math.ts`
|
**Key Files**: `src/lib/game/data/disciplines/`, `src/lib/game/stores/discipline-slice.ts`, `src/lib/game/utils/discipline-math.ts`
|
||||||
|
|
||||||
### Guardian & Spire System
|
### Guardian & Spire System
|
||||||
Every 10th floor is a guardian encounter. Guardians progress through four tiers of complexity:
|
|
||||||
|
Every 10th floor is a guardian encounter. Guardians progress through multiple tiers of complexity:
|
||||||
|
|
||||||
1. **Base Elements (Floors 10–80)**: One guardian per base element + Transference. Static definitions with named guardians (Ignis Prime, Aqua Regia, etc.). Defeating them unlocks their associated mana types.
|
1. **Base Elements (Floors 10–80)**: One guardian per base element + Transference. Static definitions with named guardians (Ignis Prime, Aqua Regia, etc.). Defeating them unlocks their associated mana types.
|
||||||
2. **Compound Elements (Floors 90–110)**: Metal, Sand, and Lightning guardians with procedurally generated names.
|
2. **Composite Elements (Floors 90–160)**: 8 composite element guardians (Metal, Sand, Lightning, Frost, BlackFlame, RadiantFlames, Miasma, ShadowGlass) with procedurally generated names.
|
||||||
3. **Exotic Elements (Floors 120–140)**: Crystal, Stellar, and Void guardians - the most powerful single-element encounters.
|
3. **Exotic Elements (Floors 170–240)**: Crystal, Stellar, Void, Soul, Time, and Plasma guardians.
|
||||||
4. **Combination Bosses (Floor 150+)**: Fully procedural dual-element guardians. Each one wields two base elements simultaneously (e.g. Fire+Water, Light+Dark) and grows stronger every 10 floors.
|
4. **Combination Bosses (Floor 250+)**: Fully procedural multi-element guardians through 8 scaling tiers, growing stronger every 10 floors.
|
||||||
|
|
||||||
**Key Files**: `src/lib/game/data/guardian-data.ts`, `src/lib/game/data/guardian-encounters.ts`
|
**Key Files**: `src/lib/game/data/guardian-data.ts`, `src/lib/game/data/guardian-encounters.ts`
|
||||||
|
|
||||||
### Combat System
|
### Combat System
|
||||||
|
|
||||||
- Cast-speed based spell casting with elemental effectiveness multipliers
|
- Cast-speed based spell casting with elemental effectiveness multipliers
|
||||||
- Enemy modifiers: Armored, Agile, Mage (barrier), Shielded, Swarm
|
- Enemy modifiers: Armored, Agile, Mage (barrier), Shielded, Swarm
|
||||||
- Golem allies deal automatic damage each tick
|
- Golem allies deal automatic damage each tick
|
||||||
@@ -266,25 +255,29 @@ Every 10th floor is a guardian encounter. Guardians progress through four tiers
|
|||||||
**Key Files**: `src/lib/game/stores/combatStore.ts`, `src/lib/game/utils/combat-utils.ts`, `src/lib/game/utils/enemy-generator.ts`
|
**Key Files**: `src/lib/game/stores/combatStore.ts`, `src/lib/game/utils/combat-utils.ts`, `src/lib/game/utils/enemy-generator.ts`
|
||||||
|
|
||||||
### Enchanting System
|
### Enchanting System
|
||||||
|
|
||||||
3-stage equipment enchantment process:
|
3-stage equipment enchantment process:
|
||||||
1. **Design**: Choose effects for your equipment type
|
1. **Design**: Choose effects for your equipment type
|
||||||
2. **Prepare**: Ready equipment (ONLY stage where disenchanting is possible)
|
2. **Prepare**: Ready equipment (ONLY stage where disenchanting is possible)
|
||||||
3. **Apply**: Apply designed enchantments (cannot re-enchant already enchanted gear)
|
3. **Apply**: Apply designed enchantments
|
||||||
|
|
||||||
**Key Files**: `src/lib/game/crafting-actions/`, `src/lib/game/data/enchantments/`
|
**Key Files**: `src/lib/game/crafting-actions/`, `src/lib/game/data/enchantments/`
|
||||||
|
|
||||||
### Golemancy System
|
### Golemancy System
|
||||||
- **Base Golems**: Earth (Fabricator 2), Steel (Metal), Crystal, Sand
|
|
||||||
|
- **Base Golems**: Earth (Fabricator 2)
|
||||||
|
- **Elemental Golems**: Steel (Metal), Crystal, Sand
|
||||||
- **Hybrid Golems** (Enchanter 5 + Fabricator 5): Lava, Galvanic, Obsidian, Prism, Quicksilver, Voidstone
|
- **Hybrid Golems** (Enchanter 5 + Fabricator 5): Lava, Galvanic, Obsidian, Prism, Quicksilver, Voidstone
|
||||||
- **Golem Slots**: 1 slot at Fabricator Level 2, +1 every 2 levels (max 5 at Level 10)
|
- **Golem Slots**: 1 slot at Fabricator Level 2, +1 every 2 levels (max 5 at Level 10)
|
||||||
|
|
||||||
**Key Files**: `src/lib/game/data/golems/`, `src/lib/game/stores/gameStore.ts`
|
**Key Files**: `src/lib/game/data/golems/`, `src/lib/game/stores/gameStore.ts`
|
||||||
|
|
||||||
### Prestige (Insight)
|
### Prestige (Insight)
|
||||||
|
|
||||||
Reset progress to gain Insight currency for permanent upgrades:
|
Reset progress to gain Insight currency for permanent upgrades:
|
||||||
- Signed pacts persist through prestige
|
- Signed pacts persist through prestige
|
||||||
- Attunement choices affect gameplay (Enchanter/Invoker/Fabricator)
|
- Attunement choices affect gameplay (Enchanter/Invoker/Fabricator)
|
||||||
- Insight upgrades provide bonuses across all loops
|
- 14 insight upgrade types provide bonuses across all loops
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -302,14 +295,17 @@ docker run -p 3000:3000 mana-loop
|
|||||||
```
|
```
|
||||||
|
|
||||||
### CI/CD Pipeline
|
### CI/CD Pipeline
|
||||||
|
|
||||||
- **Gitea Actions**: `.gitea/workflows/docker-build.yaml` automatically builds and pushes Docker images to `gitea.tailf367e3.ts.net/anexim/mana-loop:latest` on push to `master`/`main`
|
- **Gitea Actions**: `.gitea/workflows/docker-build.yaml` automatically builds and pushes Docker images to `gitea.tailf367e3.ts.net/anexim/mana-loop:latest` on push to `master`/`main`
|
||||||
- **Multi-platform**: Builds for linux/amd64 architecture
|
- **Multi-platform**: Builds for linux/amd64 architecture
|
||||||
- **Image Tags**: Branch name, commit SHA, "latest"
|
- **Image Tags**: Branch name, commit SHA, "latest"
|
||||||
|
|
||||||
### Reverse Proxy
|
### Reverse Proxy
|
||||||
|
|
||||||
A `Caddyfile` is included for reverse proxy setup (forwards port 81 to 3000).
|
A `Caddyfile` is included for reverse proxy setup (forwards port 81 to 3000).
|
||||||
|
|
||||||
### Production Build
|
### Production Build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run build
|
bun run build
|
||||||
NODE_ENV=production bun .next/standalone/server.js
|
NODE_ENV=production bun .next/standalone/server.js
|
||||||
@@ -322,6 +318,7 @@ NODE_ENV=production bun .next/standalone/server.js
|
|||||||
We welcome contributions! Please follow these guidelines:
|
We welcome contributions! Please follow these guidelines:
|
||||||
|
|
||||||
### Development Workflow
|
### Development Workflow
|
||||||
|
|
||||||
1. **Pull latest changes** before starting work: `git pull origin master`
|
1. **Pull latest changes** before starting work: `git pull origin master`
|
||||||
2. **Create a feature branch** for your changes: `git checkout -b feature/your-feature`
|
2. **Create a feature branch** for your changes: `git checkout -b feature/your-feature`
|
||||||
3. **Follow existing patterns** in the codebase (see AGENTS.md)
|
3. **Follow existing patterns** in the codebase (see AGENTS.md)
|
||||||
@@ -330,6 +327,7 @@ We welcome contributions! Please follow these guidelines:
|
|||||||
6. **Commit and push** to your branch, then create a pull request
|
6. **Commit and push** to your branch, then create a pull request
|
||||||
|
|
||||||
### Code Style
|
### Code Style
|
||||||
|
|
||||||
- TypeScript throughout with strict typing
|
- TypeScript throughout with strict typing
|
||||||
- Use existing shadcn/ui components over custom implementations
|
- Use existing shadcn/ui components over custom implementations
|
||||||
- Follow the modular store pattern (`src/lib/game/stores/`)
|
- Follow the modular store pattern (`src/lib/game/stores/`)
|
||||||
@@ -337,6 +335,7 @@ We welcome contributions! Please follow these guidelines:
|
|||||||
- Use path aliases: `@/*` maps to `./src/*`
|
- Use path aliases: `@/*` maps to `./src/*`
|
||||||
|
|
||||||
### Adding New Features
|
### Adding New Features
|
||||||
|
|
||||||
For detailed patterns on adding new effects, disciplines, spells, or systems, see the comprehensive [AGENTS.md](./AGENTS.md) guide, which includes architecture overview, coding patterns, and git workflow.
|
For detailed patterns on adding new effects, disciplines, spells, or systems, see the comprehensive [AGENTS.md](./AGENTS.md) guide, which includes architecture overview, coding patterns, and git workflow.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -346,21 +345,24 @@ For detailed patterns on adding new effects, disciplines, spells, or systems, se
|
|||||||
The following content has been removed from the game and must not be re-added:
|
The following content has been removed from the game and must not be re-added:
|
||||||
|
|
||||||
### Banned Mechanics
|
### Banned Mechanics
|
||||||
- **Lifesteal** - Player cannot heal from dealing damage
|
|
||||||
- **Healing** - Player cannot heal themselves (floors take damage, not the player)
|
- **Lifesteal** — Player cannot heal from dealing damage
|
||||||
- **Scroll crafting** - Violates the no-instant-finishing design pillar
|
- **Healing** — Player cannot heal themselves (floors take damage, not the player)
|
||||||
- **Ascension skills** - Removed; no replacement
|
- **Scroll crafting** — Violates the no-instant-finishing design pillar
|
||||||
|
- **Ascension skills** — Removed; no replacement
|
||||||
|
|
||||||
### Banned Mana Types
|
### Banned Mana Types
|
||||||
- **Life** - Removed (healing theme conflicts with core design)
|
|
||||||
- **Blood** - Removed (life derivative)
|
- **Life** — Removed (healing theme conflicts with core design)
|
||||||
- **Wood** - Removed (life derivative)
|
- **Blood** — Removed (life derivative)
|
||||||
- **Mental** - Removed
|
- **Wood** — Removed (life derivative)
|
||||||
- **Force** - Removed
|
- **Mental** — Removed
|
||||||
|
- **Force** — Removed
|
||||||
|
|
||||||
### Banned Systems
|
### Banned Systems
|
||||||
- **Familiar System** - Removed in favour of Golemancy and Pact systems
|
|
||||||
- **Skill System** (study, tiers T1–T5, milestone upgrades) - Fully replaced by the Discipline System
|
- **Familiar System** — Removed in favour of Golemancy and Pact systems
|
||||||
|
- **Skill System** (study, tiers T1–T5, milestone upgrades) — Fully replaced by the Discipline System
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -385,7 +387,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
```
|
```
|
||||||
@@ -396,7 +398,7 @@ SOFTWARE.
|
|||||||
|
|
||||||
- Built with modern web technologies (Next.js, React, TypeScript, Tailwind CSS)
|
- Built with modern web technologies (Next.js, React, TypeScript, Tailwind CSS)
|
||||||
- UI components from [shadcn/ui](https://ui.shadcn.com/)
|
- UI components from [shadcn/ui](https://ui.shadcn.com/)
|
||||||
- State management with [Zustand](https://github.com/pmndrs/zustand)
|
- State management with [Zustand](https://github.com/pmndrs/zustand/)
|
||||||
- Game icons from [Lucide React](https://lucide.dev/)
|
- Game icons from [Lucide React](https://lucide.dev/)
|
||||||
- Special thanks to the open-source community for the amazing tools that make this project possible.
|
- Special thanks to the open-source community for the amazing tools that make this project possible.
|
||||||
|
|
||||||
@@ -404,4 +406,4 @@ SOFTWARE.
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<em>Climb the spire. Master the mana. Uncover the loop.</em>
|
<em>Climb the spire. Master the mana. Uncover the loop.</em>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+29
-21
@@ -1,7 +1,7 @@
|
|||||||
# Mana-Loop: Comprehensive Game Briefing Document
|
# Mana-Loop: Comprehensive Game Briefing Document
|
||||||
|
|
||||||
**Document Version:** 3.0
|
**Document Version:** 4.0
|
||||||
**Updated:** Post-refactoring — skills removed, disciplines procedural guardians, combo bosses, 14 mana types, localStorage-only
|
**Updated:** Post-refactoring — 22 mana types, 8-tier guardian system, 64 disciplines, 8 Zustand stores, localStorage-only
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -35,11 +35,11 @@
|
|||||||
- Equipment-based spell system (spells come from enchanted gear and learned spells)
|
- Equipment-based spell system (spells come from enchanted gear and learned spells)
|
||||||
- Practice-based Discipline system — no discrete skill levels, only continuous XP growth
|
- Practice-based Discipline system — no discrete skill levels, only continuous XP growth
|
||||||
- Time pressure through the incursion mechanic (starts day 5)
|
- Time pressure through the incursion mechanic (starts day 5)
|
||||||
- Guardian progression: base → compound → exotic → combination bosses (150+)
|
- Guardian progression: base (10-80) → composite (90-160) → exotic (170-240) → combo bosses (250+, 8 tiers)
|
||||||
- Guardian pacts provide permanent multipliers that persist through prestige
|
- Guardian pacts provide permanent multipliers that persist through prestige
|
||||||
- No backend — pure client-side with localStorage persistence
|
- No backend — pure client-side with localStorage persistence
|
||||||
|
|
||||||
**Code Architecture:** Modular Zustand stores, crafting actions, discipline data, and constants. No legacy store files remain.
|
**Code Architecture:** 8 modular Zustand stores, crafting actions, discipline data, and constants. No legacy store files remain.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ ┌─────────────────────────────────────────────────────┐ │
|
│ ┌─────────────────────────────────────────────────────┐ │
|
||||||
│ │ DEFEAT GUARDIANS → SIGN PACTS (every 10th) │ │
|
│ │ DEFEAT GUARDIANS → SIGN PACTS (every 10th) │ │
|
||||||
│ │ Base(10-80) → Compound(90-110) → Exotic(120-140)│ │
|
│ │ Base(10-80) → Composite(90-160) → Exotic(170-240)│ │
|
||||||
│ │ → Combination Bosses(150+) │ │
|
│ │ → Combo Bosses(250+, 8 tiers) │ │
|
||||||
│ └─────────────────────────────────────────────────────┘ │
|
│ └─────────────────────────────────────────────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
@@ -376,7 +376,7 @@ Miasma ↔ Light Shadow Glass ↔ Light
|
|||||||
|
|
||||||
Floors 90–240 have procedurally generated names via `generateGuardianName()`.
|
Floors 90–240 have procedurally generated names via `generateGuardianName()`.
|
||||||
|
|
||||||
#### Tier 4 — Combination Bosses (Floor 150+)
|
#### Tier 4 — Dual Element Pairs (Floors 250–280)
|
||||||
|
|
||||||
Nine dual-element combinations cycle every 10 floors:
|
Nine dual-element combinations cycle every 10 floors:
|
||||||
|
|
||||||
@@ -392,14 +392,20 @@ Nine dual-element combinations cycle every 10 floors:
|
|||||||
| 7 | air + light | Radiant wind |
|
| 7 | air + light | Radiant wind |
|
||||||
| 8 | earth + death | Fossil |
|
| 8 | earth + death | Fossil |
|
||||||
|
|
||||||
**Scaling (per floor above 150):**
|
#### Tiers 5–8 — Scaling Combo Bosses (Floors 290+)
|
||||||
|
|
||||||
|
- **Tier 5 (290–330)**: Dual composite + components
|
||||||
|
- **Tier 6 (340–380)**: Exotic + components
|
||||||
|
- **Tier 7 (390–430)**: Exotic + composite + components
|
||||||
|
- **Tier 8 (440+)**: Full fusion — 1 exotic + 2 composites + all base elements
|
||||||
|
|
||||||
|
**Scaling (per tier):**
|
||||||
```
|
```
|
||||||
armor = min(0.5, 0.25 + (floor - 150) × 0.002)
|
armor = min(0.7, 0.30 + floor_increment × 0.002-0.003)
|
||||||
pactMultiplier = 6.0 + (floor - 150) × 0.05
|
pactMultiplier = 7.5+ (scales with tier)
|
||||||
pactCost = floor(hp × 0.5)
|
pactTime = 20+ hours (scales with tier)
|
||||||
pactTime = 20 + floor((floor - 150) / 10)
|
damageMult = 3.5+ (scales with tier)
|
||||||
damageMult = 3.0 + (floor - 150) × 0.02
|
insightMult = 3.0+ (scales with tier)
|
||||||
insightMult = 2.5 + (floor - 150) × 0.01
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Combo guardians grant boons to both elements (+10% each) and dual-aspect perks (+20% effectiveness to both element spells). Names generated from combined prefixes (e.g., "Ignis-Aqua the Warden").
|
Combo guardians grant boons to both elements (+10% each) and dual-aspect perks (+20% effectiveness to both element spells). Names generated from combined prefixes (e.g., "Ignis-Aqua the Warden").
|
||||||
@@ -950,7 +956,7 @@ getElementalBonus(spellElement, floorElement):
|
|||||||
|
|
||||||
### Modular Structure Overview
|
### Modular Structure Overview
|
||||||
|
|
||||||
#### Store Architecture (7 Zustand Stores)
|
#### Store Architecture (8 Zustand Stores)
|
||||||
|
|
||||||
**Active Stores (`src/lib/game/stores/`):**
|
**Active Stores (`src/lib/game/stores/`):**
|
||||||
- **gameStore.ts** — Coordinator/tick pipeline, combines all stores
|
- **gameStore.ts** — Coordinator/tick pipeline, combines all stores
|
||||||
@@ -962,6 +968,8 @@ getElementalBonus(spellElement, floorElement):
|
|||||||
- **discipline-slice.ts** — Discipline activation, XP ticking, perk evaluation
|
- **discipline-slice.ts** — Discipline activation, XP ticking, perk evaluation
|
||||||
- **uiStore.ts** — Logs, pause, game over/victory flags
|
- **uiStore.ts** — Logs, pause, game over/victory flags
|
||||||
|
|
||||||
|
(Note: `useDisciplineStore` is exported from `discipline-slice.ts`, making it the 8th store.)
|
||||||
|
|
||||||
**Supporting store files:**
|
**Supporting store files:**
|
||||||
- `tick-pipeline.ts` — `buildTickContext()` / `applyTickWrites()` pattern
|
- `tick-pipeline.ts` — `buildTickContext()` / `applyTickWrites()` pattern
|
||||||
- `combat-actions.ts` — Combat tick processing
|
- `combat-actions.ts` — Combat tick processing
|
||||||
@@ -974,12 +982,12 @@ getElementalBonus(spellElement, floorElement):
|
|||||||
**No legacy store files remain.** The old `store.ts`, `store/`, and `store-modules/` directories have been fully removed.
|
**No legacy store files remain.** The old `store.ts`, `store/`, and `store-modules/` directories have been fully removed.
|
||||||
|
|
||||||
#### Data Layer (`src/lib/game/data/`)
|
#### Data Layer (`src/lib/game/data/`)
|
||||||
- `disciplines/` — 11 files: per-attunement discipline definitions (34 disciplines total)
|
- `disciplines/` — 11 files: per-attunement discipline definitions (64 disciplines total)
|
||||||
- `enchantments/` — 7 files + `spell-effects/` subdirectory: enchantment effects by category
|
- `enchantments/` — 7 files + `spell-effects/` subdirectory: enchantment effects by category
|
||||||
- `equipment/` — 13 files: equipment type definitions by slot
|
- `equipment/` — 13 files: equipment type definitions by slot
|
||||||
- `golems/` — 7 files: golem definitions (base, elemental, hybrid)
|
- `golems/` — 7 files: golem definitions (base, elemental, hybrid)
|
||||||
- `guardian-data.ts` — Static guardian definitions (floors 10–140)
|
- `guardian-data.ts` — Static guardian definitions (floors 10–240)
|
||||||
- `guardian-encounters.ts` — Procedural guardian lookup & combo bosses (150+)
|
- `guardian-encounters.ts` — Procedural guardian lookup & combo bosses (250+, 8 tiers)
|
||||||
- `attunements.ts` — Attunement definitions
|
- `attunements.ts` — Attunement definitions
|
||||||
- `achievements.ts` — Achievement definitions (24 achievements)
|
- `achievements.ts` — Achievement definitions (24 achievements)
|
||||||
- `crafting-recipes.ts` / `fabricator-recipes.ts` — Crafting recipes
|
- `crafting-recipes.ts` / `fabricator-recipes.ts` — Crafting recipes
|
||||||
@@ -992,7 +1000,7 @@ getElementalBonus(spellElement, floorElement):
|
|||||||
- `prestige.ts` — Prestige upgrade definitions
|
- `prestige.ts` — Prestige upgrade definitions
|
||||||
- `rooms.ts` — Room type configs (armor, speed, swarm, puzzle)
|
- `rooms.ts` — Room type configs (armor, speed, swarm, puzzle)
|
||||||
- `spells.ts` — Spell constants barrel
|
- `spells.ts` — Spell constants barrel
|
||||||
- `spells-modules/` — 10 files: spell definitions by category (raw, basic, advanced, master, legendary, lightning, compound, AOE, utility, enchantment)
|
- `spells-modules/` — 15 files: spell definitions by category (raw, basic, advanced, master, legendary, lightning, compound, compound+, frost, blackflame, radiantflames, miasma, shadowglass, AOE, utility, enchantment, plasma, soul, time)
|
||||||
|
|
||||||
#### Types (`src/lib/game/types/`)
|
#### Types (`src/lib/game/types/`)
|
||||||
- `game.ts` — Core game types (GameState, ActivityLogEntry, etc.)
|
- `game.ts` — Core game types (GameState, ActivityLogEntry, etc.)
|
||||||
@@ -1094,10 +1102,10 @@ The following systems no longer exist and should not be re-introduced:
|
|||||||
| Scroll crafting | Deleted (violates no-instant-finishing pillar) |
|
| Scroll crafting | Deleted (violates no-instant-finishing pillar) |
|
||||||
| Lifesteal/healing | Banned permanently |
|
| Lifesteal/healing | Banned permanently |
|
||||||
| Familiar System | Replaced by Golemancy and Pact systems |
|
| Familiar System | Replaced by Golemancy and Pact systems |
|
||||||
| Legacy static guardians (named Primordialis, The Awakened One) | Procedural guardian system with 4 tiers |
|
| Legacy static guardians (named Primordialis, The Awakened One) | Procedural guardian system with 8 tiers |
|
||||||
| Prisma / database | Removed; localStorage-only persistence |
|
| Prisma / database | Removed; localStorage-only persistence |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Document Version: 3.0 — Post-Refactoring Update*
|
*Document Version: 4.0 — Updated: 8 stores, 22 mana types, 64 disciplines, 8-tier guardians, localStorage-only*
|
||||||
*End of Game Briefing Document*
|
*End of Game Briefing Document*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-01T09:05:01.898Z
|
Generated: 2026-06-01T10:58:05.599Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-01T09:04:59.927Z",
|
"generated": "2026-06-01T10:58:03.834Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
@@ -650,6 +650,7 @@
|
|||||||
"stores/gameStore.types.ts",
|
"stores/gameStore.types.ts",
|
||||||
"stores/manaStore.ts",
|
"stores/manaStore.ts",
|
||||||
"stores/pipelines/combat-tick.ts",
|
"stores/pipelines/combat-tick.ts",
|
||||||
|
"stores/pipelines/enchanting-tick.ts",
|
||||||
"stores/pipelines/pact-ritual.ts",
|
"stores/pipelines/pact-ritual.ts",
|
||||||
"stores/prestigeStore.ts",
|
"stores/prestigeStore.ts",
|
||||||
"stores/tick-pipeline.ts",
|
"stores/tick-pipeline.ts",
|
||||||
@@ -687,6 +688,15 @@
|
|||||||
"effects/special-effects.ts",
|
"effects/special-effects.ts",
|
||||||
"effects/upgrade-effects.types.ts"
|
"effects/upgrade-effects.types.ts"
|
||||||
],
|
],
|
||||||
|
"stores/pipelines/enchanting-tick.ts": [
|
||||||
|
"constants.ts",
|
||||||
|
"crafting-apply.ts",
|
||||||
|
"crafting-design.ts",
|
||||||
|
"crafting-prep.ts",
|
||||||
|
"effects/upgrade-effects.types.ts",
|
||||||
|
"stores/craftingStore.ts",
|
||||||
|
"stores/tick-pipeline.ts"
|
||||||
|
],
|
||||||
"stores/pipelines/equipment-crafting.ts": [
|
"stores/pipelines/equipment-crafting.ts": [
|
||||||
"crafting-equipment.ts",
|
"crafting-equipment.ts",
|
||||||
"crafting-fabricator.ts",
|
"crafting-fabricator.ts",
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── PrestigeTab.test.ts
|
│ │ │ │ ├── PrestigeTab.test.ts
|
||||||
│ │ │ │ ├── PrestigeTab.tsx
|
│ │ │ │ ├── PrestigeTab.tsx
|
||||||
│ │ │ │ ├── SpellsTab.tsx
|
│ │ │ │ ├── SpellsTab.tsx
|
||||||
|
│ │ │ │ ├── SpireSummaryTab.helpers.tsx
|
||||||
│ │ │ │ ├── SpireSummaryTab.test.ts
|
│ │ │ │ ├── SpireSummaryTab.test.ts
|
||||||
│ │ │ │ ├── SpireSummaryTab.tsx
|
│ │ │ │ ├── SpireSummaryTab.tsx
|
||||||
│ │ │ │ ├── StatsTab.tsx
|
│ │ │ │ ├── StatsTab.tsx
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface DisciplineCardProps {
|
|||||||
definition: DisciplineDefinition;
|
definition: DisciplineDefinition;
|
||||||
xp: number;
|
xp: number;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
|
autoPaused?: boolean;
|
||||||
activeIds: string[];
|
activeIds: string[];
|
||||||
concurrentLimit: number;
|
concurrentLimit: number;
|
||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
@@ -23,7 +24,7 @@ export interface DisciplineCardProps {
|
|||||||
// ─── Component ────────────────────────────────────────────────────────────────
|
// ─── Component ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
export const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
||||||
definition, xp, paused: isPaused, activeIds, concurrentLimit,
|
definition, xp, paused: isPaused, autoPaused, activeIds, concurrentLimit,
|
||||||
isLocked, missingPrereqs, missingSourceMana, onToggle,
|
isLocked, missingPrereqs, missingSourceMana, onToggle,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@@ -159,6 +160,13 @@ export const DisciplineCard: React.FC<DisciplineCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Auto-paused mana feedback (fix #244) */}
|
||||||
|
{isActive && isPaused && autoPaused && (
|
||||||
|
<div className="mt-2 text-xs text-amber-400 bg-amber-900/20 rounded px-2 py-1">
|
||||||
|
⏸️ Auto-paused — insufficient {manaName} mana to continue practicing.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<div className="mt-4 flex justify-end">
|
<div className="mt-4 flex justify-end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
|
||||||
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
import type { DisciplineDefinition, DisciplineState } from '@/lib/game/types/disciplines';
|
||||||
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
import { baseDisciplines } from '@/lib/game/data/disciplines/base';
|
||||||
import { elementalAttunementDisciplines } from '@/lib/game/data/disciplines/elemental';
|
import { elementalAttunementDisciplines } from '@/lib/game/data/disciplines/elemental';
|
||||||
import { elementalRegenDisciplines } from '@/lib/game/data/disciplines/elemental-regen';
|
import { elementalRegenDisciplines } from '@/lib/game/data/disciplines/elemental-regen';
|
||||||
@@ -40,7 +40,7 @@ const ATTUNEMENT_TABS: AttunementTab[] = [
|
|||||||
|
|
||||||
interface CardWrapperProps {
|
interface CardWrapperProps {
|
||||||
disc: DisciplineDefinition;
|
disc: DisciplineDefinition;
|
||||||
disciplines: Record<string, { xp: number; paused: boolean }>;
|
disciplines: Record<string, DisciplineState>;
|
||||||
activeIds: string[];
|
activeIds: string[];
|
||||||
concurrentLimit: number;
|
concurrentLimit: number;
|
||||||
elements: ReturnType<typeof useManaStore.getState>['elements'];
|
elements: ReturnType<typeof useManaStore.getState>['elements'];
|
||||||
@@ -58,6 +58,7 @@ const CardWrapper: React.FC<CardWrapperProps> = ({
|
|||||||
definition={disc}
|
definition={disc}
|
||||||
xp={discState.xp}
|
xp={discState.xp}
|
||||||
paused={discState.paused}
|
paused={discState.paused}
|
||||||
|
autoPaused={discState.autoPaused}
|
||||||
activeIds={activeIds}
|
activeIds={activeIds}
|
||||||
concurrentLimit={concurrentLimit}
|
concurrentLimit={concurrentLimit}
|
||||||
isLocked={!prereqCheck.canProceed}
|
isLocked={!prereqCheck.canProceed}
|
||||||
@@ -83,17 +84,16 @@ export const DisciplinesTab: React.FC = () => {
|
|||||||
|
|
||||||
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
const [activeAttunement, setActiveAttunement] = useState<string>('base');
|
||||||
|
|
||||||
const rawMana = useManaStore((s) => s.rawMana);
|
|
||||||
const elements = useManaStore((s) => s.elements);
|
const elements = useManaStore((s) => s.elements);
|
||||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||||
|
|
||||||
const handleToggle = useCallback((id: string, paused: boolean) => {
|
const handleToggle = useCallback((id: string, paused: boolean) => {
|
||||||
if (paused) {
|
if (paused) {
|
||||||
activate(id, { elements, signedPacts, rawMana });
|
activate(id, { elements, signedPacts });
|
||||||
} else {
|
} else {
|
||||||
deactivate(id);
|
deactivate(id);
|
||||||
}
|
}
|
||||||
}, [activate, deactivate, rawMana, elements, signedPacts]);
|
}, [activate, deactivate, elements, signedPacts]);
|
||||||
|
|
||||||
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
|
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
|
import type { DisciplineDefinition, DisciplineState } from '@/lib/game/types/disciplines';
|
||||||
import { ELEMENTS } from '@/lib/game/constants/elements';
|
import { ELEMENTS } from '@/lib/game/constants/elements';
|
||||||
import { elementalAttunementDisciplines } from '@/lib/game/data/disciplines/elemental';
|
import { elementalAttunementDisciplines } from '@/lib/game/data/disciplines/elemental';
|
||||||
import { elementalRegenDisciplines } from '@/lib/game/data/disciplines/elemental-regen';
|
import { elementalRegenDisciplines } from '@/lib/game/data/disciplines/elemental-regen';
|
||||||
@@ -49,7 +49,7 @@ function buildElementalDisciplineMap(
|
|||||||
// ─── Shared Props ─────────────────────────────────────────────────────────────
|
// ─── Shared Props ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface SharedRenderProps {
|
interface SharedRenderProps {
|
||||||
disciplines: Record<string, { xp: number; paused: boolean }>;
|
disciplines: Record<string, DisciplineState>;
|
||||||
activeIds: string[];
|
activeIds: string[];
|
||||||
concurrentLimit: number;
|
concurrentLimit: number;
|
||||||
elements: DisciplineCardProps['missingSourceMana'] extends readonly string[]
|
elements: DisciplineCardProps['missingSourceMana'] extends readonly string[]
|
||||||
@@ -96,6 +96,7 @@ const ElementalDisciplineGroup: React.FC<GroupProps & { activeIds: string[] }> =
|
|||||||
definition={def}
|
definition={def}
|
||||||
xp={discState.xp}
|
xp={discState.xp}
|
||||||
paused={discState.paused}
|
paused={discState.paused}
|
||||||
|
autoPaused={discState.autoPaused}
|
||||||
activeIds={activeIds}
|
activeIds={activeIds}
|
||||||
concurrentLimit={concurrentLimit}
|
concurrentLimit={concurrentLimit}
|
||||||
isLocked={!prereqCheck.canProceed}
|
isLocked={!prereqCheck.canProceed}
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { fmt } from '@/lib/game/stores';
|
||||||
|
import { ELEMENT_OPPOSITES, FLOOR_ELEM_CYCLE } from '@/lib/game/constants';
|
||||||
|
import { getGuardianForFloor, getAllGuardianFloors } from '@/lib/game/data/guardian-encounters';
|
||||||
|
import type { GuardianDef } from '@/lib/game/types';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { SectionHeader } from '@/components/ui/section-header';
|
||||||
|
|
||||||
|
const GUARDIAN_FLOORS = getAllGuardianFloors();
|
||||||
|
|
||||||
|
// ─── Helper: Get Counter Element ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getCounterElement(element: string): string | null {
|
||||||
|
return ELEMENT_OPPOSITES[element] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getElementColor(element: string): string {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
fire: '#FF6B35',
|
||||||
|
water: '#4ECDC4',
|
||||||
|
air: '#00D4FF',
|
||||||
|
earth: '#F4A261',
|
||||||
|
light: '#FFD700',
|
||||||
|
dark: '#9B59B6',
|
||||||
|
death: '#778CA3',
|
||||||
|
void: '#4A235A',
|
||||||
|
stellar: '#F0E68C',
|
||||||
|
};
|
||||||
|
return colors[element] || '#9CA3AF';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Guardian Stat Formatters ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function fmtArmor(armor: number | undefined): React.ReactNode {
|
||||||
|
if (!armor || armor <= 0) return null;
|
||||||
|
return (
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{'Armor: '}{Math.round(armor * 100)}{'%'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmtShield(shield: number | undefined): React.ReactNode {
|
||||||
|
if (!shield || shield <= 0) return null;
|
||||||
|
return (
|
||||||
|
<span className="text-xs text-cyan-400">
|
||||||
|
{'Shield: '}{fmt(shield)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmtBarrier(barrier: number | undefined): React.ReactNode {
|
||||||
|
if (!barrier || barrier <= 0) return null;
|
||||||
|
return (
|
||||||
|
<span className="text-xs text-blue-400">
|
||||||
|
{'Barrier: '}{Math.round(barrier * 100)}{'%'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fmtRegen(regen: number | undefined, isPercent: boolean | undefined): React.ReactNode {
|
||||||
|
if (!regen || regen <= 0) return null;
|
||||||
|
return (
|
||||||
|
<span className="text-xs text-green-400">
|
||||||
|
{'Regen: '}{isPercent ? `${regen}%/tick` : `${regen}/tick`}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Preparation Tips ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function PreparationTips({ counterElement, nextFloorElement, hasHighArmor }: {
|
||||||
|
counterElement: string | null; nextFloorElement: string | null; hasHighArmor: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-3 space-y-2">
|
||||||
|
<div className="text-xs font-medium text-gray-300">Recommended Preparation:</div>
|
||||||
|
<div className="text-xs text-gray-400 space-y-1">
|
||||||
|
{counterElement && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-emerald-400">⚡</span>
|
||||||
|
<span>
|
||||||
|
Use <span style={{ color: getElementColor(counterElement) }} className="font-medium">{counterElement}</span> spells for super effective damage (+50%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{nextFloorElement && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-blue-400">🔄</span>
|
||||||
|
<span>
|
||||||
|
Floor element: <span style={{ color: getElementColor(nextFloorElement) }} className="font-medium">{nextFloorElement}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasHighArmor && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-red-400">🛡️</span>
|
||||||
|
<span>High armor — consider armor-piercing or raw damage spells</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-amber-400">💡</span>
|
||||||
|
<span>Ensure mana pools are full before attempting</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Guardian Roster ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function GuardianRoster({ clearedFloors }: { clearedFloors: Record<number, boolean> }) {
|
||||||
|
return (
|
||||||
|
<Card className="bg-gray-900/60 border-gray-700">
|
||||||
|
<SectionHeader title="🏛️ Guardian Roster" />
|
||||||
|
<CardContent className="pt-0">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{GUARDIAN_FLOORS.map((floor) => {
|
||||||
|
const guardian = getGuardianForFloor(floor);
|
||||||
|
return guardian ? (
|
||||||
|
<GuardianRosterItem key={floor} floor={floor} guardian={guardian} isDefeated={!!clearedFloors[floor]} />
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; guardian: GuardianDef; isDefeated: boolean }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex items-center justify-between p-2 rounded border ${
|
||||||
|
isDefeated
|
||||||
|
? 'bg-emerald-900/20 border-emerald-800/40'
|
||||||
|
: 'bg-gray-800/40 border-gray-700/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className="w-7 h-7 rounded flex items-center justify-center text-xs font-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: isDefeated ? `${guardian.color}30` : '#374151',
|
||||||
|
color: isDefeated ? guardian.color : '#6B7280',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{floor}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={`text-sm font-medium ${isDefeated ? 'text-gray-100' : 'text-gray-400'}`}>
|
||||||
|
{guardian.name}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="text-[10px] px-1.5 py-0.5 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${guardian.color}15`,
|
||||||
|
color: guardian.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{guardian.element.join(' + ')}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] text-gray-500">Health: {fmt(guardian.hp)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{isDefeated ? (
|
||||||
|
<Badge variant="outline" className="border-emerald-600 text-emerald-400 text-xs">
|
||||||
|
✓ Defeated
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="border-gray-600 text-gray-500 text-xs">
|
||||||
|
Undefeated
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Floor Progress Bar ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clearedFloors: Record<number, boolean> }) {
|
||||||
|
const totalFloors = Math.min(maxFloor, 100);
|
||||||
|
const clearedSet = new Set(Object.entries(clearedFloors).filter(([, v]) => v).map(([k]) => Number(k)));
|
||||||
|
|
||||||
|
const rows: number[][] = [];
|
||||||
|
for (let i = 0; i < totalFloors; i += 10) {
|
||||||
|
rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{rows.reverse().map((row) => (
|
||||||
|
<div key={row[0]} className="flex gap-1">
|
||||||
|
{row.map((floor) => {
|
||||||
|
const isCleared = clearedSet.has(floor);
|
||||||
|
const isGuardian = !!getGuardianForFloor(floor);
|
||||||
|
const isCurrent = floor === maxFloor;
|
||||||
|
|
||||||
|
let bgClass = 'bg-gray-800';
|
||||||
|
if (isCleared) bgClass = 'bg-emerald-600/60';
|
||||||
|
else if (isCurrent) bgClass = 'bg-amber-600/60';
|
||||||
|
|
||||||
|
const borderClass = isGuardian ? 'border-amber-500' : isCurrent ? 'border-amber-400' : 'border-gray-700';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={floor}
|
||||||
|
className={`w-7 h-7 flex items-center justify-center text-[9px] rounded border ${bgClass} ${borderClass} ${isGuardian ? 'font-bold' : ''}`}
|
||||||
|
title={getGuardianForFloor(floor) ? `Floor ${floor} — ${getGuardianForFloor(floor)!.name} (${getGuardianForFloor(floor)!.element.join(' + ')})` : `Floor ${floor}${isCleared ? ' (cleared)' : ''}`}
|
||||||
|
>
|
||||||
|
{floor}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<FloorLegend />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FloorLegend() {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" />
|
||||||
|
<span>Cleared</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-gray-800 border border-gray-700" />
|
||||||
|
<span>Uncleared</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-gray-800 border border-amber-500" />
|
||||||
|
<span>Guardian</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-3 h-3 rounded bg-amber-600/60 border border-amber-400" />
|
||||||
|
<span>Current</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,125 +2,23 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { useCombatStore, usePrestigeStore, fmt } from '@/lib/game/stores';
|
import { useCombatStore, usePrestigeStore } from '@/lib/game/stores';
|
||||||
import { ELEMENT_OPPOSITES, FLOOR_ELEM_CYCLE } from '@/lib/game/constants';
|
import { FLOOR_ELEM_CYCLE } from '@/lib/game/constants';
|
||||||
import { getGuardianForFloor, getAllGuardianFloors } from '@/lib/game/data/guardian-encounters';
|
import { getGuardianForFloor } from '@/lib/game/data/guardian-encounters';
|
||||||
import type { GuardianDef } from '@/lib/game/types';
|
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { SectionHeader } from '@/components/ui/section-header';
|
import { SectionHeader } from '@/components/ui/section-header';
|
||||||
import { DebugName } from '@/components/game/debug/debug-context';
|
import { DebugName } from '@/components/game/debug/debug-context';
|
||||||
|
import {
|
||||||
|
getCounterElement, getElementColor, fmtArmor, fmtShield, fmtBarrier, fmtRegen,
|
||||||
// ─── Guardian Data ────────────────────────────────────────────────────────────
|
PreparationTips, GuardianRoster, FloorProgressBar,
|
||||||
|
} from './SpireSummaryTab.helpers';
|
||||||
const GUARDIAN_FLOORS = getAllGuardianFloors();
|
|
||||||
|
|
||||||
// ─── Helper: Get Counter Element ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
function getCounterElement(element: string): string | null {
|
|
||||||
return ELEMENT_OPPOSITES[element] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getElementColor(element: string): string {
|
|
||||||
const colors: Record<string, string> = {
|
|
||||||
fire: '#FF6B35',
|
|
||||||
water: '#4ECDC4',
|
|
||||||
air: '#00D4FF',
|
|
||||||
earth: '#F4A261',
|
|
||||||
light: '#FFD700',
|
|
||||||
dark: '#9B59B6',
|
|
||||||
death: '#778CA3',
|
|
||||||
void: '#4A235A',
|
|
||||||
stellar: '#F0E68C',
|
|
||||||
};
|
|
||||||
return colors[element] || '#9CA3AF';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Sub-component: Floor Progress Bar ────────────────────────────────────────
|
|
||||||
|
|
||||||
function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clearedFloors: Record<number, boolean> }) {
|
|
||||||
const totalFloors = Math.min(maxFloor, 100);
|
|
||||||
const clearedSet = new Set(Object.entries(clearedFloors).filter(([, v]) => v).map(([k]) => Number(k)));
|
|
||||||
|
|
||||||
const rows: number[][] = [];
|
|
||||||
for (let i = 0; i < totalFloors; i += 10) {
|
|
||||||
rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-1">
|
|
||||||
{rows.reverse().map((row) => (
|
|
||||||
<div key={row[0]} className="flex gap-1">
|
|
||||||
{row.map((floor) => {
|
|
||||||
const isCleared = clearedSet.has(floor);
|
|
||||||
const isGuardian = !!getGuardianForFloor(floor);
|
|
||||||
const isCurrent = floor === maxFloor;
|
|
||||||
|
|
||||||
let bgClass = 'bg-gray-800';
|
|
||||||
if (isCleared) bgClass = 'bg-emerald-600/60';
|
|
||||||
else if (isCurrent) bgClass = 'bg-amber-600/60';
|
|
||||||
|
|
||||||
const borderClass = isGuardian
|
|
||||||
? 'border-amber-500'
|
|
||||||
: isCurrent
|
|
||||||
? 'border-amber-400'
|
|
||||||
: 'border-gray-700';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={floor}
|
|
||||||
className={`w-7 h-7 flex items-center justify-center text-[9px] rounded border ${bgClass} ${borderClass} ${
|
|
||||||
isGuardian ? 'font-bold' : ''
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
getGuardianForFloor(floor)
|
|
||||||
? `Floor ${floor} — ${getGuardianForFloor(floor)!.name} (${getGuardianForFloor(floor)!.element.join(' + ')})`
|
|
||||||
: `Floor ${floor}${isCleared ? ' (cleared)' : ''}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{floor}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<FloorLegend />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FloorLegend() {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" />
|
|
||||||
<span>Cleared</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-gray-800 border border-gray-700" />
|
|
||||||
<span>Uncleared</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-gray-800 border border-amber-500" />
|
|
||||||
<span>Guardian</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<div className="w-3 h-3 rounded bg-amber-600/60 border border-amber-400" />
|
|
||||||
<span>Current</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Top Stats Row ───────────────────────────────────────────────────────────
|
// ─── Top Stats Row ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function TopStatsRow({ maxFloorReached, totalFloorsCleared, defeatedCount, insight }: {
|
function TopStatsRow({ maxFloorReached, totalFloorsCleared, defeatedCount, insight }: {
|
||||||
maxFloorReached: number;
|
maxFloorReached: number; totalFloorsCleared: number; defeatedCount: number; insight: number;
|
||||||
totalFloorsCleared: number;
|
|
||||||
defeatedCount: number;
|
|
||||||
insight: number;
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card className="bg-gray-900/60 border-gray-700">
|
<Card className="bg-gray-900/60 border-gray-700">
|
||||||
@@ -129,17 +27,17 @@ function TopStatsRow({ maxFloorReached, totalFloorsCleared, defeatedCount, insig
|
|||||||
<StatCell value={maxFloorReached} label="Max Floor Reached" color="text-amber-400" />
|
<StatCell value={maxFloorReached} label="Max Floor Reached" color="text-amber-400" />
|
||||||
<StatCell value={totalFloorsCleared} label="Floors Cleared" color="text-gray-200" />
|
<StatCell value={totalFloorsCleared} label="Floors Cleared" color="text-gray-200" />
|
||||||
<StatCell value={defeatedCount} label="Guardians Defeated" color="text-emerald-400" />
|
<StatCell value={defeatedCount} label="Guardians Defeated" color="text-emerald-400" />
|
||||||
<StatCell value={fmt(insight)} label="Insight Earned" color="text-purple-400" />
|
<StatCell value={insight} label="Insight Earned" color="text-purple-400" isFmt />
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatCell({ value, label, color }: { value: number | string; label: string; color: string }) {
|
function StatCell({ value, label, color, isFmt }: { value: number; label: string; color: string; isFmt?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className={`text-2xl font-bold ${color}`}>{value}</div>
|
<div className={`text-2xl font-bold ${color}`}>{isFmt ? value : value}</div>
|
||||||
<div className="text-xs text-gray-400 mt-0.5">{label}</div>
|
<div className="text-xs text-gray-400 mt-0.5">{label}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -147,16 +45,14 @@ function StatCell({ value, label, color }: { value: number | string; label: stri
|
|||||||
|
|
||||||
// ─── Next Guardian Card ──────────────────────────────────────────────────────
|
// ─── Next Guardian Card ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: number; nextGuardianData: GuardianDef }) {
|
function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: number; nextGuardianData: ReturnType<typeof getGuardianForFloor> }) {
|
||||||
|
if (!nextGuardianData) return null;
|
||||||
const counterElement = getCounterElement(nextGuardianData.element[0]);
|
const counterElement = getCounterElement(nextGuardianData.element[0]);
|
||||||
const nextFloorElement = FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length];
|
const nextFloorElement = FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-gray-900/60 border-amber-800/40">
|
<Card className="bg-gray-900/60 border-amber-800/40">
|
||||||
<SectionHeader
|
<SectionHeader title={`🛡️ Next Guardian — Floor ${nextGuardian}`} className="text-amber-400" />
|
||||||
title={`🛡️ Next Guardian — Floor ${nextGuardian}`}
|
|
||||||
className="text-amber-400"
|
|
||||||
/>
|
|
||||||
<CardContent className="pt-0 space-y-3">
|
<CardContent className="pt-0 space-y-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -171,204 +67,45 @@ function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: nu
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-gray-100">{nextGuardianData.name}</div>
|
<div className="font-semibold text-gray-100">{nextGuardianData.name}</div>
|
||||||
<div className="flex items-center gap-2 mt-0.5">
|
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
||||||
<Badge
|
<Badge variant="outline" className="text-xs" style={{ borderColor: getElementColor(nextGuardianData.element[0]), color: getElementColor(nextGuardianData.element[0]) }}>
|
||||||
variant="outline"
|
|
||||||
className="text-xs"
|
|
||||||
style={{ borderColor: getElementColor(nextGuardianData.element[0]), color: getElementColor(nextGuardianData.element[0]) }}
|
|
||||||
>
|
|
||||||
{nextGuardianData.element.join(' + ')}
|
{nextGuardianData.element.join(' + ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-xs text-gray-500">Health: {fmt(nextGuardianData.hp)}</span>
|
<span className="text-xs text-gray-500">Health: {nextGuardianData.hp}</span>
|
||||||
{nextGuardianData.armor && (
|
{fmtArmor(nextGuardianData.armor)}
|
||||||
<span className="text-xs text-gray-500">
|
{fmtShield(nextGuardianData.shield)}
|
||||||
Armor: {Math.round(nextGuardianData.armor * 100)}%
|
{fmtBarrier(nextGuardianData.barrier)}
|
||||||
</span>
|
{fmtRegen(nextGuardianData.healthRegen, nextGuardianData.healthRegenIsPercent)}
|
||||||
)}
|
|
||||||
{nextGuardianData.shield && nextGuardianData.shield > 0 && (
|
|
||||||
<span className="text-xs text-cyan-400">
|
|
||||||
Shield: {fmt(nextGuardianData.shield)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{nextGuardianData.barrier && nextGuardianData.barrier > 0 && (
|
|
||||||
<span className="text-xs text-blue-400">
|
|
||||||
Barrier: {Math.round(nextGuardianData.barrier * 100)}%
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{nextGuardianData.healthRegen && nextGuardianData.healthRegen > 0 && (
|
|
||||||
<span className="text-xs text-green-400">
|
|
||||||
Regen: {nextGuardianData.healthRegenIsPercent ? nextGuardianData.healthRegen + '%/tick' : nextGuardianData.healthRegen + '/tick'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<PreparationTips counterElement={counterElement} nextFloorElement={nextFloorElement} hasHighArmor={!!(nextGuardianData.armor && nextGuardianData.armor > 0.15)} />
|
||||||
<PreparationTips
|
|
||||||
counterElement={counterElement}
|
|
||||||
nextFloorElement={nextFloorElement}
|
|
||||||
hasHighArmor={!!(nextGuardianData.armor && nextGuardianData.armor > 0.15)}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PreparationTips({ counterElement, nextFloorElement, hasHighArmor }: { counterElement: string | null; nextFloorElement: string | null; hasHighArmor: boolean }) {
|
|
||||||
return (
|
|
||||||
<div className="bg-gray-800/50 rounded-lg p-3 space-y-2">
|
|
||||||
<div className="text-xs font-medium text-gray-300">Recommended Preparation:</div>
|
|
||||||
<div className="text-xs text-gray-400 space-y-1">
|
|
||||||
{counterElement && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-emerald-400">⚡</span>
|
|
||||||
<span>
|
|
||||||
Use <span style={{ color: getElementColor(counterElement) }} className="font-medium">{counterElement}</span> spells for super effective damage (+50%)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{nextFloorElement && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-blue-400">🔄</span>
|
|
||||||
<span>
|
|
||||||
Floor element: <span style={{ color: getElementColor(nextFloorElement) }} className="font-medium">{nextFloorElement}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasHighArmor && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-red-400">🛡️</span>
|
|
||||||
<span>High armor — consider armor-piercing or raw damage spells</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-amber-400">💡</span>
|
|
||||||
<span>Ensure mana pools are full before attempting</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Guardian Roster ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function GuardianRoster({ clearedFloors }: { clearedFloors: Record<number, boolean> }) {
|
|
||||||
return (
|
|
||||||
<Card className="bg-gray-900/60 border-gray-700">
|
|
||||||
<SectionHeader title="🏛️ Guardian Roster" />
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{GUARDIAN_FLOORS.map((floor) => {
|
|
||||||
const guardian = getGuardianForFloor(floor);
|
|
||||||
return guardian ? (
|
|
||||||
<GuardianRosterItem key={floor} floor={floor} guardian={guardian} isDefeated={!!clearedFloors[floor]} />
|
|
||||||
) : null;
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; guardian: GuardianDef; isDefeated: boolean }) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-between p-2 rounded border ${
|
|
||||||
isDefeated
|
|
||||||
? 'bg-emerald-900/20 border-emerald-800/40'
|
|
||||||
: 'bg-gray-800/40 border-gray-700/50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="w-7 h-7 rounded flex items-center justify-center text-xs font-bold"
|
|
||||||
style={{
|
|
||||||
backgroundColor: isDefeated ? `${guardian.color}30` : '#374151',
|
|
||||||
color: isDefeated ? guardian.color : '#6B7280',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{floor}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className={`text-sm font-medium ${isDefeated ? 'text-gray-100' : 'text-gray-400'}`}>
|
|
||||||
{guardian.name}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className="text-[10px] px-1.5 py-0.5 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${guardian.color}15`,
|
|
||||||
color: guardian.color,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{guardian.element.join(' + ')}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] text-gray-500">Health: {fmt(guardian.hp)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{isDefeated ? (
|
|
||||||
<Badge variant="outline" className="border-emerald-600 text-emerald-400 text-xs">
|
|
||||||
✓ Defeated
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="outline" className="border-gray-600 text-gray-500 text-xs">
|
|
||||||
Undefeated
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function SpireSummaryTab() {
|
export function SpireSummaryTab() {
|
||||||
const {
|
const { maxFloorReached, clearedFloors, enterSpireMode } = useCombatStore(useShallow((s) => ({
|
||||||
maxFloorReached,
|
|
||||||
clearedFloors,
|
|
||||||
enterSpireMode,
|
|
||||||
} = useCombatStore(useShallow((s) => ({
|
|
||||||
maxFloorReached: s.maxFloorReached,
|
maxFloorReached: s.maxFloorReached,
|
||||||
clearedFloors: s.clearedFloors,
|
clearedFloors: s.clearedFloors,
|
||||||
enterSpireMode: s.enterSpireMode,
|
enterSpireMode: s.enterSpireMode,
|
||||||
})));
|
})));
|
||||||
|
const { insight } = usePrestigeStore(useShallow((s) => ({ insight: s.insight })));
|
||||||
|
|
||||||
const { insight } = usePrestigeStore(useShallow((s) => ({
|
const defeatedGuardians = useMemo(() => getAllGuardianFloors().filter((floor) => clearedFloors[floor]), [clearedFloors]);
|
||||||
insight: s.insight,
|
const nextGuardian = useMemo(() => getAllGuardianFloors().find((floor) => !clearedFloors[floor]) || null, [clearedFloors]);
|
||||||
})));
|
|
||||||
|
|
||||||
const defeatedGuardians = useMemo(() => {
|
|
||||||
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
|
|
||||||
}, [clearedFloors]);
|
|
||||||
|
|
||||||
const nextGuardian = useMemo(() => {
|
|
||||||
return GUARDIAN_FLOORS.find((floor) => !clearedFloors[floor]) || null;
|
|
||||||
}, [clearedFloors]);
|
|
||||||
|
|
||||||
const nextGuardianData = nextGuardian ? getGuardianForFloor(nextGuardian) : null;
|
const nextGuardianData = nextGuardian ? getGuardianForFloor(nextGuardian) : null;
|
||||||
|
const totalFloorsCleared = useMemo(() => Object.values(clearedFloors).filter(Boolean).length, [clearedFloors]);
|
||||||
const totalFloorsCleared = useMemo(() => {
|
|
||||||
return Object.values(clearedFloors).filter(Boolean).length;
|
|
||||||
}, [clearedFloors]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DebugName name="SpireSummaryTab">
|
<DebugName name="SpireSummaryTab">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<TopStatsRow
|
<TopStatsRow maxFloorReached={maxFloorReached} totalFloorsCleared={totalFloorsCleared} defeatedCount={defeatedGuardians.length} insight={insight} />
|
||||||
maxFloorReached={maxFloorReached}
|
{nextGuardianData && nextGuardian && <NextGuardianCard nextGuardian={nextGuardian} nextGuardianData={nextGuardianData} />}
|
||||||
totalFloorsCleared={totalFloorsCleared}
|
|
||||||
defeatedCount={defeatedGuardians.length}
|
|
||||||
insight={insight}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{nextGuardianData && nextGuardian && (
|
|
||||||
<NextGuardianCard nextGuardian={nextGuardian} nextGuardianData={nextGuardianData} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<GuardianRoster clearedFloors={clearedFloors} />
|
<GuardianRoster clearedFloors={clearedFloors} />
|
||||||
|
|
||||||
<Card className="bg-gray-900/60 border-gray-700">
|
<Card className="bg-gray-900/60 border-gray-700">
|
||||||
<SectionHeader title="🗺️ Floor Progress" />
|
<SectionHeader title="🗺️ Floor Progress" />
|
||||||
<CardContent className="pt-0">
|
<CardContent className="pt-0">
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
exitSpireMode: () => {
|
exitSpireMode: () => {
|
||||||
set({
|
set((s) => ({
|
||||||
spireMode: false,
|
spireMode: false,
|
||||||
currentAction: 'meditate',
|
currentAction: 'meditate',
|
||||||
climbDirection: null,
|
climbDirection: null,
|
||||||
@@ -164,8 +164,9 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
currentRoom: generateFloorState(1),
|
currentRoom: generateFloorState(1),
|
||||||
castProgress: 0,
|
castProgress: 0,
|
||||||
clearedFloors: {},
|
clearedFloors: {},
|
||||||
maxFloorReached: 0,
|
// Preserve maxFloorReached — don't reset to 0 on spire exit (fix #238)
|
||||||
});
|
maxFloorReached: Math.max(s.maxFloorReached, 1),
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
startClimbUp: () => set({ climbDirection: 'up', currentAction: 'climb' }),
|
startClimbUp: () => set({ climbDirection: 'up', currentAction: 'climb' }),
|
||||||
@@ -219,7 +220,7 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
climbDirection: null,
|
climbDirection: null,
|
||||||
isDescending: false,
|
isDescending: false,
|
||||||
clearedFloors: {},
|
clearedFloors: {},
|
||||||
maxFloorReached: Math.max(s.maxFloorReached, s.currentFloor),
|
// Don't inflate maxFloorReached — it should reflect actual progress (fix #238)
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export interface DisciplineStoreActions {
|
|||||||
elements: Record<string, ElementState>;
|
elements: Record<string, ElementState>;
|
||||||
unlockedEffects: string[];
|
unlockedEffects: string[];
|
||||||
unlockedRecipes: string[];
|
unlockedRecipes: string[];
|
||||||
|
autoPausedNames: string[];
|
||||||
};
|
};
|
||||||
setPracticingCallbacks(callbacks: { onStartPracticing: () => void; onStopPracticing: () => void }): void;
|
setPracticingCallbacks(callbacks: { onStartPracticing: () => void; onStopPracticing: () => void }): void;
|
||||||
resetDisciplines: () => void;
|
resetDisciplines: () => void;
|
||||||
@@ -83,10 +84,10 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
// Allow re-activation if discipline exists but is paused
|
// Allow re-activation if discipline exists but is paused
|
||||||
const existing = s.disciplines[id];
|
const existing = s.disciplines[id];
|
||||||
if (activeIds.includes(id)) {
|
if (activeIds.includes(id)) {
|
||||||
// If already active and paused, un-pause it
|
// If already active and paused (manually or auto), un-pause it
|
||||||
if (existing?.paused) {
|
if (existing?.paused) {
|
||||||
return {
|
return {
|
||||||
disciplines: { ...s.disciplines, [id]: { ...existing, paused: false } },
|
disciplines: { ...s.disciplines, [id]: { ...existing, paused: false, autoPaused: false } },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return s;
|
return s;
|
||||||
@@ -166,10 +167,12 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
const newProcessedPerks = [...(s.processedPerks ?? [])];
|
const newProcessedPerks = [...(s.processedPerks ?? [])];
|
||||||
|
|
||||||
const drainedIds: string[] = [];
|
const drainedIds: string[] = [];
|
||||||
|
const drainedNames: string[] = [];
|
||||||
for (const id of s.activeIds ?? []) {
|
for (const id of s.activeIds ?? []) {
|
||||||
const disc = newDisciplines[id];
|
const disc = newDisciplines[id];
|
||||||
if (!disc) continue;
|
if (!disc) continue;
|
||||||
if (disc.paused) continue;
|
if (disc.paused) continue;
|
||||||
|
if (disc.autoPaused) continue; // already auto-paused, don't re-process
|
||||||
|
|
||||||
const def = DISCIPLINE_MAP[id];
|
const def = DISCIPLINE_MAP[id];
|
||||||
if (!def) continue;
|
if (!def) continue;
|
||||||
@@ -179,8 +182,9 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
const available = def.manaType === 'raw' ? rawMana : element?.current;
|
const available = def.manaType === 'raw' ? rawMana : element?.current;
|
||||||
|
|
||||||
if (!available || available < drain) {
|
if (!available || available < drain) {
|
||||||
newDisciplines[id] = { ...disc, paused: true };
|
newDisciplines[id] = { ...disc, paused: true, autoPaused: true };
|
||||||
drainedIds.push(id);
|
drainedIds.push(id);
|
||||||
|
drainedNames.push(def.name);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +265,7 @@ export const useDisciplineStore = create<DisciplineStore>()(
|
|||||||
processedPerks: newProcessedPerks,
|
processedPerks: newProcessedPerks,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { rawMana, elements, unlockedEffects: newUnlockedEffects, unlockedRecipes: newUnlockedRecipes };
|
return { rawMana, elements, unlockedEffects: newUnlockedEffects, unlockedRecipes: newUnlockedRecipes, autoPausedNames: drainedNames };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{ storage: createSafeStorage(), name: 'mana-loop-discipline-store', version: 1, partialize: (state) => ({ disciplines: state.disciplines, activeIds: state.activeIds, concurrentLimit: state.concurrentLimit, totalXP: state.totalXP, processedPerks: state.processedPerks }) }
|
{ storage: createSafeStorage(), name: 'mana-loop-discipline-store', version: 1, partialize: (state) => ({ disciplines: state.disciplines, activeIds: state.activeIds, concurrentLimit: state.concurrentLimit, totalXP: state.totalXP, processedPerks: state.processedPerks }) }
|
||||||
|
|||||||
@@ -218,6 +218,21 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
rawMana = disciplineResult.rawMana;
|
rawMana = disciplineResult.rawMana;
|
||||||
elements = disciplineResult.elements;
|
elements = disciplineResult.elements;
|
||||||
|
|
||||||
|
// Log auto-paused disciplines for better UX feedback (fix #244)
|
||||||
|
if (disciplineResult.autoPausedNames.length > 0) {
|
||||||
|
const names = disciplineResult.autoPausedNames.join(', ');
|
||||||
|
addLog('⏸️ Auto-paused (insufficient mana): ' + names);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute maxMana after discipline XP gains so clamping uses updated value (fix #246)
|
||||||
|
const updatedDisciplineEffects = computeDisciplineEffects();
|
||||||
|
const updatedMaxMana = computeMaxMana(
|
||||||
|
{ prestigeUpgrades: ctx.prestige.prestigeUpgrades },
|
||||||
|
undefined,
|
||||||
|
updatedDisciplineEffects,
|
||||||
|
);
|
||||||
|
rawMana = Math.min(rawMana, updatedMaxMana);
|
||||||
|
|
||||||
for (const [targetElem, conv] of Object.entries(disciplineEffects.conversions)) {
|
for (const [targetElem, conv] of Object.entries(disciplineEffects.conversions)) {
|
||||||
const conversionAmount = conv.rate * HOURS_PER_TICK;
|
const conversionAmount = conv.rate * HOURS_PER_TICK;
|
||||||
let canConvert = true;
|
let canConvert = true;
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export interface DisciplineState {
|
|||||||
id: string;
|
id: string;
|
||||||
xp: number;
|
xp: number;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
|
/** True when the discipline was auto-paused due to insufficient mana */
|
||||||
|
autoPaused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Discipline Constants ─────────────────────────────────────────────────────
|
// ─── Discipline Constants ─────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user