fix: bugs #238,#240,#244,#246 + docs #248 update
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:
2026-06-01 13:54:28 +02:00
parent 7dd9ad5b92
commit fa78c7a93a
15 changed files with 448 additions and 407 deletions
+11 -8
View File
@@ -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 1080):** 7 base elements + Transference, static definitions with unique names - **Base (floors 1080):** 7 base elements + Transference, static definitions with unique names
- **Compound (floors 90110):** Metal, Sand, Lightning — procedurally named - **Tier 2 — Composite (floors 90160):** 8 composite elements (Metal, Sand, Lightning, Frost, BlackFlame, RadiantFlames, Miasma, ShadowGlass)
- **Exotic (floors 120140):** Crystal, Stellar, Void — procedurally named - **Tier 3 — Exotic (floors 170240):** 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)
+68 -66
View File
@@ -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 (1080), compound (90110), exotic (120140), then procedural combination bosses (150+) - Every 10th floor is a guardian: base elements (1080), composite (90160), exotic (170240), 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)
@@ -106,20 +112,17 @@
| **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 10140) │ │ ├── guardian-data.ts # Static guardian definitions (floors 10240)
│ │ └── 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 1080)**: 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 1080)**: 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 90110)**: Metal, Sand, and Lightning guardians with procedurally generated names. 2. **Composite Elements (Floors 90160)**: 8 composite element guardians (Metal, Sand, Lightning, Frost, BlackFlame, RadiantFlames, Miasma, ShadowGlass) with procedurally generated names.
3. **Exotic Elements (Floors 120140)**: Crystal, Stellar, and Void guardians - the most powerful single-element encounters. 3. **Exotic Elements (Floors 170240)**: 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 T1T5, milestone upgrades) - Fully replaced by the Discipline System - **Familiar System** — Removed in favour of Golemancy and Pact systems
- **Skill System** (study, tiers T1T5, 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.
+29 -21
View File
@@ -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 90240 have procedurally generated names via `generateGuardianName()`. Floors 90240 have procedurally generated names via `generateGuardianName()`.
#### Tier 4 — Combination Bosses (Floor 150+) #### Tier 4 — Dual Element Pairs (Floors 250280)
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 58 — Scaling Combo Bosses (Floors 290+)
- **Tier 5 (290330)**: Dual composite + components
- **Tier 6 (340380)**: Exotic + components
- **Tier 7 (390430)**: 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 10140) - `guardian-data.ts` — Static guardian definitions (floors 10240)
- `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 -1
View File
@@ -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. ✅
+11 -1
View File
@@ -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",
+1
View File
@@ -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
+9 -1
View File
@@ -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
+5 -5
View File
@@ -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);
+3 -2
View File
@@ -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>
);
}
+29 -292
View File
@@ -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">
+5 -4
View File
@@ -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)
})); }));
}, },
+8 -4
View File
@@ -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 }) }
+15
View File
@@ -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;
+2
View File
@@ -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 ─────────────────────────────────────────────────────