Compare commits
2 Commits
2ca5d8b7f8
...
751b317af2
| Author | SHA1 | Date | |
|---|---|---|---|
| 751b317af2 | |||
| 315490cedb |
121
AGENTS.md
121
AGENTS.md
@@ -53,26 +53,37 @@ This document provides a comprehensive overview of the project architecture for
|
||||
```
|
||||
src/
|
||||
├── app/
|
||||
│ ├── page.tsx # Main game UI (single page application)
|
||||
│ ├── page.tsx # Main game UI (~1700 lines, single page application)
|
||||
│ ├── layout.tsx # Root layout with providers
|
||||
│ └── api/ # API routes (minimal use)
|
||||
├── components/
|
||||
│ ├── ui/ # shadcn/ui components (auto-generated)
|
||||
│ └── game/
|
||||
│ ├── index.ts # Barrel exports
|
||||
│ └── tabs/ # Tab-specific components
|
||||
│ ├── CraftingTab.tsx
|
||||
│ ├── LabTab.tsx
|
||||
│ ├── SpellsTab.tsx
|
||||
│ └── SpireTab.tsx
|
||||
│ ├── ActionButtons.tsx # Main action buttons (Meditate, Climb, Study, etc.)
|
||||
│ ├── CalendarDisplay.tsx # Day calendar with incursion indicators
|
||||
│ ├── CraftingProgress.tsx # Design/preparation/application progress bars
|
||||
│ ├── StudyProgress.tsx # Current study progress with cancel button
|
||||
│ ├── ManaDisplay.tsx # Mana/gathering section with progress bar
|
||||
│ ├── TimeDisplay.tsx # Day/hour display with pause toggle
|
||||
│ └── tabs/ # Tab-specific components
|
||||
│ ├── index.ts # Tab component exports
|
||||
│ ├── CraftingTab.tsx # Enchantment crafting UI
|
||||
│ ├── LabTab.tsx # Skill upgrade and lab features
|
||||
│ ├── SpellsTab.tsx # Spell management and equipment spells
|
||||
│ └── SpireTab.tsx # Combat and spire climbing
|
||||
└── lib/
|
||||
├── game/
|
||||
│ ├── store.ts # Zustand store (state + actions)
|
||||
│ ├── store.ts # Zustand store (~1650 lines, main state + tick logic)
|
||||
│ ├── computed-stats.ts # Computed stats functions (extracted utilities)
|
||||
│ ├── navigation-slice.ts # Floor navigation actions (setClimbDirection, changeFloor)
|
||||
│ ├── study-slice.ts # Study system actions (startStudying*, cancelStudy)
|
||||
│ ├── crafting-slice.ts # Equipment/enchantment logic
|
||||
│ ├── familiar-slice.ts # Familiar system actions
|
||||
│ ├── effects.ts # Unified effect computation
|
||||
│ ├── upgrade-effects.ts # Skill upgrade effect definitions
|
||||
│ ├── constants.ts # Game definitions (spells, skills, etc.)
|
||||
│ ├── skill-evolution.ts # Skill tier progression paths
|
||||
│ ├── crafting-slice.ts # Equipment/enchantment logic
|
||||
│ ├── types.ts # TypeScript interfaces
|
||||
│ ├── formatting.ts # Display formatters
|
||||
│ ├── utils.ts # Utility functions
|
||||
@@ -86,7 +97,21 @@ src/
|
||||
|
||||
### 1. State Management (`store.ts`)
|
||||
|
||||
The game uses a single Zustand store with the following key slices:
|
||||
The game uses a Zustand store organized with **slice pattern** for better maintainability:
|
||||
|
||||
#### Store Slices
|
||||
- **Main Store** (`store.ts`): Core state, tick logic, and main actions
|
||||
- **Navigation Slice** (`navigation-slice.ts`): Floor navigation (setClimbDirection, changeFloor)
|
||||
- **Study Slice** (`study-slice.ts`): Study system (startStudyingSkill, startStudyingSpell, cancelStudy)
|
||||
- **Crafting Slice** (`crafting-slice.ts`): Equipment/enchantment (createEquipmentInstance, startDesigningEnchantment)
|
||||
- **Familiar Slice** (`familiar-slice.ts`): Familiar system (addFamiliar, removeFamiliar)
|
||||
|
||||
#### Computed Stats (`computed-stats.ts`)
|
||||
Extracted utility functions for stat calculations:
|
||||
- `computeMaxMana()`, `computeRegen()`, `computeEffectiveRegen()`
|
||||
- `calcDamage()`, `calcInsight()`, `getElementalBonus()`
|
||||
- `getFloorMaxHP()`, `getFloorElement()`, `getMeditationBonus()`
|
||||
- `canAffordSpellCost()`, `deductSpellCost()`
|
||||
|
||||
```typescript
|
||||
interface GameState {
|
||||
@@ -239,8 +264,82 @@ damage *= effects.myNewStatMultiplier;
|
||||
- Check dev server logs at `/home/z/my-project/dev.log`
|
||||
- Test with fresh game state (clear localStorage)
|
||||
|
||||
## Slice Pattern for Store Organization
|
||||
|
||||
The store uses a **slice pattern** to organize related actions into separate files. This improves maintainability and makes the codebase more modular.
|
||||
|
||||
### Creating a New Slice
|
||||
|
||||
1. **Create the slice file** (e.g., `my-feature-slice.ts`):
|
||||
```typescript
|
||||
// Define the actions interface
|
||||
export interface MyFeatureActions {
|
||||
doSomething: (param: string) => void;
|
||||
undoSomething: () => void;
|
||||
}
|
||||
|
||||
// Create the slice factory
|
||||
export function createMyFeatureSlice(
|
||||
set: StoreApi<GameStore>['setState'],
|
||||
get: StoreApi<GameStore>['getState']
|
||||
): MyFeatureActions {
|
||||
return {
|
||||
doSomething: (param: string) => {
|
||||
set((state) => {
|
||||
// Update state
|
||||
});
|
||||
},
|
||||
undoSomething: () => {
|
||||
set((state) => {
|
||||
// Update state
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add to main store** (`store.ts`):
|
||||
```typescript
|
||||
import { createMyFeatureSlice, MyFeatureActions } from './my-feature-slice';
|
||||
|
||||
// Extend GameStore interface
|
||||
interface GameStore extends GameState, MyFeatureActions, /* other slices */ {}
|
||||
|
||||
// Spread into store creation
|
||||
const useGameStore = create<GameStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...createMyFeatureSlice(set, get),
|
||||
// other slices and state
|
||||
}),
|
||||
// persist config
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Existing Slices
|
||||
|
||||
| Slice | File | Purpose |
|
||||
|-------|------|---------|
|
||||
| Navigation | `navigation-slice.ts` | Floor navigation (setClimbDirection, changeFloor) |
|
||||
| Study | `study-slice.ts` | Study system (startStudyingSkill, startStudyingSpell, cancelStudy) |
|
||||
| Crafting | `crafting-slice.ts` | Equipment/enchantment (createEquipmentInstance, startDesigningEnchantment) |
|
||||
| Familiar | `familiar-slice.ts` | Familiar system (addFamiliar, removeFamiliar) |
|
||||
|
||||
## File Size Guidelines
|
||||
|
||||
- Keep `page.tsx` under 2000 lines by extracting to tab components
|
||||
- Keep store functions focused; extract to helper files when >50 lines
|
||||
### Current File Sizes (After Refactoring)
|
||||
| File | Lines | Notes |
|
||||
|------|-------|-------|
|
||||
| `store.ts` | ~1650 | Core state + tick logic (reduced from 2138, 23% reduction) |
|
||||
| `page.tsx` | ~1695 | Main UI (reduced from 2554, 34% reduction) |
|
||||
| `computed-stats.ts` | ~200 | Extracted utility functions |
|
||||
| `navigation-slice.ts` | ~50 | Navigation actions |
|
||||
| `study-slice.ts` | ~100 | Study system actions |
|
||||
|
||||
### Guidelines
|
||||
- Keep `page.tsx` under 2000 lines by extracting to components (ActionButtons, ManaDisplay, etc.)
|
||||
- Keep `store.ts` under 1800 lines by extracting to slices (navigation, study, crafting, familiar)
|
||||
- Extract computed stats and utility functions to `computed-stats.ts` when >50 lines
|
||||
- Use barrel exports (`index.ts`) for clean imports
|
||||
- Follow the slice pattern for store organization (see below)
|
||||
|
||||
313
AUDIT_REPORT.md
Normal file
313
AUDIT_REPORT.md
Normal file
@@ -0,0 +1,313 @@
|
||||
# Mana Loop - Codebase Audit Report
|
||||
|
||||
**Task ID:** 4
|
||||
**Date:** Audit of unimplemented effects, upgrades, and missing functionality
|
||||
|
||||
---
|
||||
|
||||
## 1. Special Effects Status
|
||||
|
||||
### SPECIAL_EFFECTS Constant (upgrade-effects.ts)
|
||||
|
||||
The `SPECIAL_EFFECTS` constant defines 32 special effect IDs. Here's the implementation status:
|
||||
|
||||
| Effect ID | Name | Status | Notes |
|
||||
|-----------|------|--------|-------|
|
||||
| `MANA_CASCADE` | Mana Cascade | ⚠️ Partially Implemented | Defined in `computeDynamicRegen()` but that function is NOT called from store.ts |
|
||||
| `STEADY_STREAM` | Steady Stream | ⚠️ Partially Implemented | Defined in `computeDynamicRegen()` but not called from tick |
|
||||
| `MANA_TORRENT` | Mana Torrent | ⚠️ Partially Implemented | Defined in `computeDynamicRegen()` but not called |
|
||||
| `FLOW_SURGE` | Flow Surge | ❌ Missing | Not implemented anywhere |
|
||||
| `MANA_EQUILIBRIUM` | Mana Equilibrium | ❌ Missing | Not implemented |
|
||||
| `DESPERATE_WELLS` | Desperate Wells | ⚠️ Partially Implemented | Defined in `computeDynamicRegen()` but not called |
|
||||
| `MANA_ECHO` | Mana Echo | ❌ Missing | Not implemented in gatherMana() |
|
||||
| `EMERGENCY_RESERVE` | Emergency Reserve | ❌ Missing | Not implemented in startNewLoop() |
|
||||
| `BATTLE_FURY` | Battle Fury | ⚠️ Partially Implemented | In `computeDynamicDamage()` but function not called |
|
||||
| `ARMOR_PIERCE` | Armor Pierce | ❌ Missing | Floor defense not implemented |
|
||||
| `OVERPOWER` | Overpower | ✅ Implemented | store.ts line 627 |
|
||||
| `BERSERKER` | Berserker | ✅ Implemented | store.ts line 632 |
|
||||
| `COMBO_MASTER` | Combo Master | ❌ Missing | Not implemented |
|
||||
| `ADRENALINE_RUSH` | Adrenaline Rush | ❌ Missing | Not implemented on enemy defeat |
|
||||
| `PERFECT_MEMORY` | Perfect Memory | ❌ Missing | Not implemented in cancel study |
|
||||
| `QUICK_MASTERY` | Quick Mastery | ❌ Missing | Not implemented |
|
||||
| `PARALLEL_STUDY` | Parallel Study | ⚠️ Partially Implemented | State exists but logic incomplete |
|
||||
| `STUDY_INSIGHT` | Study Insight | ❌ Missing | Not implemented |
|
||||
| `STUDY_MOMENTUM` | Study Momentum | ❌ Missing | Not implemented |
|
||||
| `KNOWLEDGE_ECHO` | Knowledge Echo | ❌ Missing | Not implemented |
|
||||
| `KNOWLEDGE_TRANSFER` | Knowledge Transfer | ❌ Missing | Not implemented |
|
||||
| `MENTAL_CLARITY` | Mental Clarity | ❌ Missing | Not implemented |
|
||||
| `STUDY_REFUND` | Study Refund | ❌ Missing | Not implemented |
|
||||
| `FREE_STUDY` | Free Study | ❌ Missing | Not implemented |
|
||||
| `MIND_PALACE` | Mind Palace | ❌ Missing | Not implemented |
|
||||
| `STUDY_RUSH` | Study Rush | ❌ Missing | Not implemented |
|
||||
| `CHAIN_STUDY` | Chain Study | ❌ Missing | Not implemented |
|
||||
| `ELEMENTAL_HARMONY` | Elemental Harmony | ❌ Missing | Not implemented |
|
||||
| `DEEP_STORAGE` | Deep Storage | ❌ Missing | Not implemented |
|
||||
| `DOUBLE_CRAFT` | Double Craft | ❌ Missing | Not implemented |
|
||||
| `ELEMENTAL_RESONANCE` | Elemental Resonance | ❌ Missing | Not implemented |
|
||||
| `PURE_ELEMENTS` | Pure Elements | ❌ Missing | Not implemented |
|
||||
|
||||
**Summary:** 2 fully implemented, 6 partially implemented (function exists but not called), 24 not implemented.
|
||||
|
||||
---
|
||||
|
||||
## 2. Enchantment Effects Status
|
||||
|
||||
### Equipment Enchantment Effects (enchantment-effects.ts)
|
||||
|
||||
The following effect types are defined:
|
||||
|
||||
| Effect Type | Status | Notes |
|
||||
|-------------|--------|-------|
|
||||
| **Spell Effects** (`type: 'spell'`) | ✅ Working | Spells granted via `getSpellsFromEquipment()` |
|
||||
| **Bonus Effects** (`type: 'bonus'`) | ✅ Working | Applied in `computeEquipmentEffects()` |
|
||||
| **Multiplier Effects** (`type: 'multiplier'`) | ✅ Working | Applied in `computeEquipmentEffects()` |
|
||||
| **Special Effects** (`type: 'special'`) | ⚠️ Tracked Only | Added to `specials` Set but NOT applied in game logic |
|
||||
|
||||
### Special Enchantment Effects Not Applied:
|
||||
|
||||
| Effect ID | Description | Issue |
|
||||
|-----------|-------------|-------|
|
||||
| `spellEcho10` | 10% chance cast twice | Tracked but not implemented in combat |
|
||||
| `lifesteal5` | 5% damage as mana | Tracked but not implemented in combat |
|
||||
| `overpower` | +50% damage at 80% mana | Tracked but separate from skill upgrade version |
|
||||
|
||||
**Location of Issue:**
|
||||
```typescript
|
||||
// effects.ts line 58-60
|
||||
} else if (effect.type === 'special' && effect.specialId) {
|
||||
specials.add(effect.specialId);
|
||||
}
|
||||
// Effect is tracked but never used in combat/damage calculations
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Skill Effects Status
|
||||
|
||||
### SKILLS_DEF Analysis (constants.ts)
|
||||
|
||||
Skills with direct effects that should apply per level:
|
||||
|
||||
| Skill | Effect | Status |
|
||||
|-------|--------|--------|
|
||||
| `manaWell` | +100 max mana per level | ✅ Implemented |
|
||||
| `manaFlow` | +1 regen/hr per level | ✅ Implemented |
|
||||
| `elemAttune` | +50 elem mana cap | ✅ Implemented |
|
||||
| `manaOverflow` | +25% click mana | ✅ Implemented |
|
||||
| `quickLearner` | +10% study speed | ✅ Implemented |
|
||||
| `focusedMind` | -5% study cost | ✅ Implemented |
|
||||
| `meditation` | 2.5x regen after 4hrs | ✅ Implemented |
|
||||
| `knowledgeRetention` | +20% progress saved | ⚠️ Partially Implemented |
|
||||
| `enchanting` | Unlocks designs | ✅ Implemented |
|
||||
| `efficientEnchant` | -5% capacity cost | ⚠️ Not verified |
|
||||
| `disenchanting` | 20% mana recovery | ⚠️ Not verified |
|
||||
| `enchantSpeed` | -10% enchant time | ⚠️ Not verified |
|
||||
| `scrollCrafting` | Create scrolls | ❌ Not implemented |
|
||||
| `essenceRefining` | +10% effect power | ⚠️ Not verified |
|
||||
| `effCrafting` | -10% craft time | ⚠️ Not verified |
|
||||
| `fieldRepair` | +15% repair | ❌ Repair not implemented |
|
||||
| `elemCrafting` | +25% craft output | ✅ Implemented |
|
||||
| `manaTap` | +1 mana/click | ✅ Implemented |
|
||||
| `manaSurge` | +3 mana/click | ✅ Implemented |
|
||||
| `manaSpring` | +2 regen | ✅ Implemented |
|
||||
| `deepTrance` | 3x after 6hrs | ✅ Implemented |
|
||||
| `voidMeditation` | 5x after 8hrs | ✅ Implemented |
|
||||
| `insightHarvest` | +10% insight | ✅ Implemented |
|
||||
| `temporalMemory` | Keep spells | ✅ Implemented |
|
||||
| `guardianBane` | +20% vs guardians | ⚠️ Tracked but not verified |
|
||||
|
||||
---
|
||||
|
||||
## 4. Missing Implementations
|
||||
|
||||
### 4.1 Dynamic Effect Functions Not Called
|
||||
|
||||
The following functions exist in `upgrade-effects.ts` but are NOT called from `store.ts`:
|
||||
|
||||
```typescript
|
||||
// upgrade-effects.ts - EXISTS but NOT USED
|
||||
export function computeDynamicRegen(
|
||||
effects: ComputedEffects,
|
||||
baseRegen: number,
|
||||
maxMana: number,
|
||||
currentMana: number,
|
||||
incursionStrength: number
|
||||
): number { ... }
|
||||
|
||||
export function computeDynamicDamage(
|
||||
effects: ComputedEffects,
|
||||
baseDamage: number,
|
||||
floorHPPct: number,
|
||||
currentMana: number,
|
||||
maxMana: number,
|
||||
consecutiveHits: number
|
||||
): number { ... }
|
||||
```
|
||||
|
||||
**Where it should be called:**
|
||||
- `store.ts` tick() function around line 414 for regen
|
||||
- `store.ts` tick() function around line 618 for damage
|
||||
|
||||
### 4.2 Missing Combat Special Effects
|
||||
|
||||
Location: `store.ts` tick() combat section (lines 510-760)
|
||||
|
||||
Missing implementations:
|
||||
```typescript
|
||||
// BATTLE_FURY - +10% damage per consecutive hit
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BATTLE_FURY)) {
|
||||
// Need to track consecutiveHits in state
|
||||
}
|
||||
|
||||
// ARMOR_PIERCE - Ignore 10% floor defense
|
||||
// Floor defense not implemented in game
|
||||
|
||||
// COMBO_MASTER - Every 5th attack deals 3x damage
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.COMBO_MASTER)) {
|
||||
// Need to track hitCount in state
|
||||
}
|
||||
|
||||
// ADRENALINE_RUSH - Restore 5% mana on kill
|
||||
// Should be added after floorHP <= 0 check
|
||||
```
|
||||
|
||||
### 4.3 Missing Study Special Effects
|
||||
|
||||
Location: `store.ts` tick() study section (lines 440-485)
|
||||
|
||||
Missing implementations:
|
||||
```typescript
|
||||
// MENTAL_CLARITY - +10% study speed when mana > 75%
|
||||
// STUDY_RUSH - First hour is 2x speed
|
||||
// STUDY_REFUND - 25% mana back on completion
|
||||
// KNOWLEDGE_ECHO - 10% instant study chance
|
||||
// STUDY_MOMENTUM - +5% speed per consecutive hour
|
||||
```
|
||||
|
||||
### 4.4 Missing Loop/Click Effects
|
||||
|
||||
Location: `store.ts` gatherMana() and startNewLoop()
|
||||
|
||||
```typescript
|
||||
// gatherMana() - MANA_ECHO
|
||||
// 10% chance to gain double mana from clicks
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_ECHO) && Math.random() < 0.1) {
|
||||
cm *= 2;
|
||||
}
|
||||
|
||||
// startNewLoop() - EMERGENCY_RESERVE
|
||||
// Keep 10% max mana when starting new loop
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EMERGENCY_RESERVE)) {
|
||||
newState.rawMana = maxMana * 0.1;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 Parallel Study Incomplete
|
||||
|
||||
`parallelStudyTarget` exists in state but the logic is not fully implemented in tick():
|
||||
- State field exists (line 203)
|
||||
- No tick processing for parallel study
|
||||
- UI may show it but actual progress not processed
|
||||
|
||||
---
|
||||
|
||||
## 5. Balance Concerns
|
||||
|
||||
### 5.1 Weak Upgrades
|
||||
|
||||
| Upgrade | Issue | Suggestion |
|
||||
|---------|-------|------------|
|
||||
| `manaThreshold` | +20% mana for -10% regen is a net negative early | Change to +30% mana for -5% regen |
|
||||
| `manaOverflow` | +25% click mana at 5 levels is only +5%/level | Increase to +10% per level |
|
||||
| `fieldRepair` | Repair system not implemented | Remove or implement repair |
|
||||
| `scrollCrafting` | Scroll system not implemented | Remove or implement scrolls |
|
||||
|
||||
### 5.2 Tier Scaling Issues
|
||||
|
||||
From `skill-evolution.ts`, tier multipliers are 10x per tier:
|
||||
- Tier 1: multiplier 1
|
||||
- Tier 2: multiplier 10
|
||||
- Tier 3: multiplier 100
|
||||
- Tier 4: multiplier 1000
|
||||
- Tier 5: multiplier 10000
|
||||
|
||||
This creates massive power jumps that may trivialize content when tiering up.
|
||||
|
||||
### 5.3 Special Effect Research Costs
|
||||
|
||||
Research skills for effects are expensive but effects may not be implemented:
|
||||
- `researchSpecialEffects` costs 500 mana + 10 hours study
|
||||
- Effects like `spellEcho10` are tracked but not applied
|
||||
- Player invests resources for non-functional upgrades
|
||||
|
||||
---
|
||||
|
||||
## 6. Critical Issues
|
||||
|
||||
### 6.1 computeDynamicRegen Not Used
|
||||
|
||||
**File:** `computed-stats.ts` lines 210-225
|
||||
|
||||
The function exists but only applies incursion penalty. It should call the more comprehensive `computeDynamicRegen` from `upgrade-effects.ts` that handles:
|
||||
- Mana Cascade
|
||||
- Mana Torrent
|
||||
- Desperate Wells
|
||||
- Steady Stream
|
||||
|
||||
### 6.2 No Consecutive Hit Tracking
|
||||
|
||||
`BATTLE_FURY` and `COMBO_MASTER` require tracking consecutive hits, but this state doesn't exist. Need:
|
||||
```typescript
|
||||
// In GameState
|
||||
consecutiveHits: number;
|
||||
totalHitsThisLoop: number;
|
||||
```
|
||||
|
||||
### 6.3 Enchantment Special Effects Not Applied
|
||||
|
||||
The `specials` Set is populated but never checked in combat for enchantment-specific effects like:
|
||||
- `lifesteal5`
|
||||
- `spellEcho10`
|
||||
|
||||
---
|
||||
|
||||
## 7. Recommendations
|
||||
|
||||
### Priority 1 - Core Effects
|
||||
1. Call `computeDynamicRegen()` from tick() instead of inline calculation
|
||||
2. Call `computeDynamicDamage()` from combat section
|
||||
3. Implement MANA_ECHO in gatherMana()
|
||||
4. Implement EMERGENCY_RESERVE in startNewLoop()
|
||||
|
||||
### Priority 2 - Combat Effects
|
||||
1. Add `consecutiveHits` to GameState
|
||||
2. Implement BATTLE_FURY damage scaling
|
||||
3. Implement COMBO_MASTER every 5th hit
|
||||
4. Implement ADRENALINE_RUSH on kill
|
||||
|
||||
### Priority 3 - Study Effects
|
||||
1. Implement MENTAL_CLARITY conditional speed
|
||||
2. Implement STUDY_RUSH first hour bonus
|
||||
3. Implement STUDY_REFUND on completion
|
||||
4. Implement KNOWLEDGE_ECHO instant chance
|
||||
|
||||
### Priority 4 - Missing Systems
|
||||
1. Implement or remove `scrollCrafting` skill
|
||||
2. Implement or remove `fieldRepair` skill
|
||||
3. Complete parallel study tick processing
|
||||
4. Implement floor defense for ARMOR_PIERCE
|
||||
|
||||
---
|
||||
|
||||
## 8. Files Affected
|
||||
|
||||
| File | Changes Needed |
|
||||
|------|----------------|
|
||||
| `src/lib/game/store.ts` | Call dynamic effect functions, implement specials |
|
||||
| `src/lib/game/computed-stats.ts` | Integrate with upgrade-effects dynamic functions |
|
||||
| `src/lib/game/types.ts` | Add consecutiveHits to GameState |
|
||||
| `src/lib/game/skill-evolution.ts` | Consider removing unimplementable upgrades |
|
||||
|
||||
---
|
||||
|
||||
**End of Audit Report**
|
||||
284
README.md
Normal file
284
README.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# Mana Loop
|
||||
|
||||
An incremental/idle game about climbing a magical spire, mastering skills, and uncovering the secrets of an ancient tower.
|
||||
|
||||
## Overview
|
||||
|
||||
**Mana Loop** is a browser-based incremental game where players gather mana, study skills and spells, climb the floors of a mysterious spire, and craft enchanted equipment. The game features a prestige system (Insight) that provides permanent progression bonuses across playthroughs.
|
||||
|
||||
### The Game Loop
|
||||
|
||||
1. **Gather Mana** - Click to collect mana or let it regenerate automatically
|
||||
2. **Study Skills & Spells** - Spend mana to learn new abilities and unlock upgrades
|
||||
3. **Climb the Spire** - Battle through floors, defeat guardians, and sign pacts for power
|
||||
4. **Craft Equipment** - Enchant your gear with powerful effects
|
||||
5. **Prestige** - Reset for Insight, gaining permanent bonuses
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Mana Gathering & Management
|
||||
- Click-based mana collection with automatic regeneration
|
||||
- Elemental mana system with five elements (Fire, Water, Earth, Air, Void)
|
||||
- Mana conversion between raw and elemental forms
|
||||
- Meditation system for boosted regeneration
|
||||
|
||||
### Skill Progression with Tier Evolution
|
||||
- 20+ skills across multiple categories (mana, combat, enchanting, familiar)
|
||||
- 5-tier evolution system for each skill
|
||||
- Milestone upgrades at levels 5 and 10 for each tier
|
||||
- Unique special effects unlocked through skill upgrades
|
||||
|
||||
### Equipment Crafting & Enchanting
|
||||
- 3-stage enchantment process (Design → Prepare → Apply)
|
||||
- Equipment capacity system limiting total enchantment power
|
||||
- Enchantment effects including stat bonuses, multipliers, and spell grants
|
||||
- Disenchanting to recover mana from unwanted enchantments
|
||||
|
||||
### Combat System
|
||||
- Cast speed-based spell casting
|
||||
- Multi-spell support from equipped weapons
|
||||
- Elemental damage bonuses and effectiveness
|
||||
- Floor guardians with unique boons and pacts
|
||||
|
||||
### Familiar System
|
||||
- Collect and train magical companions
|
||||
- Familiars provide passive bonuses and active abilities
|
||||
- Growth and evolution mechanics
|
||||
|
||||
### Floor Navigation & Guardian Battles
|
||||
- Procedurally generated spire floors
|
||||
- Elemental floor themes affecting combat
|
||||
- Guardian bosses with unique mechanics
|
||||
- Pact system for permanent power boosts
|
||||
|
||||
### Prestige System (Insight)
|
||||
- Reset progress for permanent bonuses
|
||||
- Insight upgrades across multiple categories
|
||||
- Signed pacts persist through prestige
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Technology | Purpose |
|
||||
|------------|---------|
|
||||
| **Next.js 16** | Full-stack framework with App Router |
|
||||
| **TypeScript 5** | Type-safe development |
|
||||
| **Tailwind CSS 4** | Utility-first styling |
|
||||
| **shadcn/ui** | Reusable UI components |
|
||||
| **Zustand** | Client state management with persistence |
|
||||
| **Prisma ORM** | Database abstraction (SQLite) |
|
||||
| **Bun** | JavaScript runtime and package manager |
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js** 18+ or **Bun** runtime
|
||||
- **npm**, **yarn**, or **bun** package manager
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone git@gitea.tailf367e3.ts.net:Anexim/Mana-Loop.git
|
||||
cd Mana-Loop
|
||||
|
||||
# Install dependencies
|
||||
bun install
|
||||
# or
|
||||
npm install
|
||||
|
||||
# Set up the database
|
||||
bun run db:push
|
||||
# or
|
||||
npm run db:push
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
# Start the development server
|
||||
bun run dev
|
||||
# or
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The game will be available at `http://localhost:3000`.
|
||||
|
||||
### Other Commands
|
||||
|
||||
```bash
|
||||
# Run linting
|
||||
bun run lint
|
||||
|
||||
# Build for production
|
||||
bun run build
|
||||
|
||||
# Start production server
|
||||
bun run start
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/
|
||||
│ ├── page.tsx # Main game UI (single-page application)
|
||||
│ ├── layout.tsx # Root layout with providers
|
||||
│ └── api/ # API routes
|
||||
├── components/
|
||||
│ ├── ui/ # shadcn/ui components
|
||||
│ └── game/ # Game-specific components
|
||||
│ ├── tabs/ # Tab-based UI components
|
||||
│ │ ├── CraftingTab.tsx
|
||||
│ │ ├── LabTab.tsx
|
||||
│ │ ├── SpellsTab.tsx
|
||||
│ │ ├── SpireTab.tsx
|
||||
│ │ └── FamiliarTab.tsx
|
||||
│ ├── ManaDisplay.tsx
|
||||
│ ├── TimeDisplay.tsx
|
||||
│ ├── ActionButtons.tsx
|
||||
│ └── ...
|
||||
└── lib/
|
||||
├── game/
|
||||
│ ├── store.ts # Zustand store (state + actions)
|
||||
│ ├── effects.ts # Unified effect computation
|
||||
│ ├── upgrade-effects.ts # Skill upgrade definitions
|
||||
│ ├── skill-evolution.ts # Tier progression paths
|
||||
│ ├── constants.ts # Game data definitions
|
||||
│ ├── computed-stats.ts # Stat calculation functions
|
||||
│ ├── crafting-slice.ts # Equipment/enchantment actions
|
||||
│ ├── familiar-slice.ts # Familiar system actions
|
||||
│ ├── navigation-slice.ts # Floor navigation actions
|
||||
│ ├── study-slice.ts # Study system actions
|
||||
│ ├── types.ts # TypeScript interfaces
|
||||
│ ├── formatting.ts # Display formatters
|
||||
│ ├── utils.ts # Utility functions
|
||||
│ └── data/
|
||||
│ ├── equipment.ts # Equipment definitions
|
||||
│ ├── enchantment-effects.ts # Enchantment catalog
|
||||
│ ├── familiars.ts # Familiar definitions
|
||||
│ ├── crafting-recipes.ts # Crafting recipes
|
||||
│ ├── achievements.ts # Achievement definitions
|
||||
│ └── loot-drops.ts # Loot drop tables
|
||||
└── utils.ts # General utilities
|
||||
```
|
||||
|
||||
For detailed architecture documentation, see [AGENTS.md](./AGENTS.md).
|
||||
|
||||
---
|
||||
|
||||
## Game Systems Overview
|
||||
|
||||
### Mana System
|
||||
The core resource of the game. Mana is gathered manually or automatically and used for studying skills, casting spells, and crafting.
|
||||
|
||||
**Key Files:**
|
||||
- `store.ts` - Mana state and actions
|
||||
- `computed-stats.ts` - Mana calculations
|
||||
|
||||
### Skill System
|
||||
Skills provide passive bonuses and unlock new abilities. Each skill can evolve through 5 tiers with milestone upgrades.
|
||||
|
||||
**Key Files:**
|
||||
- `constants.ts` - Skill definitions (`SKILLS_DEF`)
|
||||
- `skill-evolution.ts` - Evolution paths and upgrades
|
||||
- `upgrade-effects.ts` - Effect computation
|
||||
|
||||
### Combat System
|
||||
Combat uses a cast-speed system where each spell has a unique cast rate. Damage is calculated with skill bonuses, elemental modifiers, and special effects.
|
||||
|
||||
**Key Files:**
|
||||
- `store.ts` - Combat tick logic
|
||||
- `constants.ts` - Spell definitions (`SPELLS_DEF`)
|
||||
- `effects.ts` - Damage calculations
|
||||
|
||||
### Crafting System
|
||||
A 3-stage enchantment system for equipment. Design effects, prepare equipment, and apply enchantments within capacity limits.
|
||||
|
||||
**Key Files:**
|
||||
- `crafting-slice.ts` - Crafting actions
|
||||
- `data/equipment.ts` - Equipment types
|
||||
- `data/enchantment-effects.ts` - Available effects
|
||||
|
||||
### Familiar System
|
||||
Magical companions that provide bonuses and can be trained and evolved.
|
||||
|
||||
**Key Files:**
|
||||
- `familiar-slice.ts` - Familiar actions
|
||||
- `data/familiars.ts` - Familiar definitions
|
||||
|
||||
### Prestige System
|
||||
Reset progress for Insight, which provides permanent bonuses. Signed pacts persist through prestige.
|
||||
|
||||
**Key Files:**
|
||||
- `store.ts` - Prestige logic
|
||||
- `constants.ts` - Insight upgrades
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please follow these guidelines:
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Pull the latest changes** before starting work
|
||||
2. **Create a feature branch** for your changes
|
||||
3. **Follow existing patterns** in the codebase
|
||||
4. **Run linting** before committing (`bun run lint`)
|
||||
5. **Test your changes** thoroughly
|
||||
|
||||
### Code Style
|
||||
|
||||
- TypeScript throughout with strict typing
|
||||
- Use existing shadcn/ui components over custom implementations
|
||||
- Follow the slice pattern for store actions
|
||||
- Keep components focused and extract to separate files when >50 lines
|
||||
|
||||
### Adding New Features
|
||||
|
||||
For detailed patterns on adding new effects, skills, spells, or systems, see [AGENTS.md](./AGENTS.md).
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Mana Loop
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN 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
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Built with love using modern web technologies. Special thanks to the open-source community for the amazing tools that make this project possible.
|
||||
1769
src/app/page.tsx
1769
src/app/page.tsx
File diff suppressed because it is too large
Load Diff
115
src/components/game/UpgradeDialog.tsx
Normal file
115
src/components/game/UpgradeDialog.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
|
||||
export interface UpgradeDialogProps {
|
||||
open: boolean;
|
||||
skillId: string | null;
|
||||
milestone: 5 | 10;
|
||||
pendingSelections: string[];
|
||||
available: SkillUpgradeChoice[];
|
||||
alreadySelected: string[];
|
||||
onToggle: (upgradeId: string) => void;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function UpgradeDialog({
|
||||
open,
|
||||
skillId,
|
||||
milestone,
|
||||
pendingSelections,
|
||||
available,
|
||||
alreadySelected,
|
||||
onToggle,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onOpenChange,
|
||||
}: UpgradeDialogProps) {
|
||||
if (!skillId) return null;
|
||||
|
||||
const skillDef = SKILLS_DEF[skillId];
|
||||
const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-amber-400">
|
||||
Choose Upgrade - {skillDef?.name || skillId}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Level {milestone} Milestone - Select 2 upgrades ({currentSelections.length}/2 chosen)
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2 mt-4">
|
||||
{available.map((upgrade) => {
|
||||
const isSelected = currentSelections.includes(upgrade.id);
|
||||
const canToggle = currentSelections.length < 2 || isSelected;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={upgrade.id}
|
||||
className={`p-3 rounded border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-amber-500 bg-amber-900/30'
|
||||
: canToggle
|
||||
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
|
||||
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (canToggle) {
|
||||
onToggle(upgrade.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
|
||||
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||||
{upgrade.effect.type === 'multiplier' && (
|
||||
<div className="text-xs text-green-400 mt-1">
|
||||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'bonus' && (
|
||||
<div className="text-xs text-blue-400 mt-1">
|
||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'special' && (
|
||||
<div className="text-xs text-cyan-400 mt-1">
|
||||
⚡ {upgrade.effect.specialDesc || 'Special effect'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={onConfirm}
|
||||
disabled={currentSelections.length !== 2}
|
||||
>
|
||||
{currentSelections.length < 2 ? `Select ${2 - currentSelections.length} more` : 'Confirm'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ export { CraftingTab } from './tabs/CraftingTab';
|
||||
export { SpireTab } from './tabs/SpireTab';
|
||||
export { SpellsTab } from './tabs/SpellsTab';
|
||||
export { LabTab } from './tabs/LabTab';
|
||||
export { SkillsTab } from './tabs/SkillsTab';
|
||||
export { StatsTab } from './tabs/StatsTab';
|
||||
|
||||
// UI components
|
||||
export { ActionButtons } from './ActionButtons';
|
||||
@@ -15,3 +17,4 @@ export { CraftingProgress } from './CraftingProgress';
|
||||
export { StudyProgress } from './StudyProgress';
|
||||
export { ManaDisplay } from './ManaDisplay';
|
||||
export { TimeDisplay } from './TimeDisplay';
|
||||
export { UpgradeDialog } from './UpgradeDialog';
|
||||
|
||||
338
src/components/game/tabs/SkillsTab.tsx
Normal file
338
src/components/game/tabs/SkillsTab.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { SKILLS_DEF, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { fmt, fmtDec } from '@/lib/game/store';
|
||||
import { formatStudyTime } from '@/lib/game/formatting';
|
||||
import type { SkillUpgradeChoice, GameStore } from '@/lib/game/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { StudyProgress } from './StudyProgress';
|
||||
import { UpgradeDialog } from './UpgradeDialog';
|
||||
|
||||
export interface SkillsTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
// Check if skill has milestone available
|
||||
function hasMilestoneUpgrade(
|
||||
skillId: string,
|
||||
level: number,
|
||||
skillTiers: Record<string, number>,
|
||||
skillUpgrades: Record<string, string[]>
|
||||
): { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null {
|
||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
|
||||
if (!path) return null;
|
||||
|
||||
// Check level 5 milestone
|
||||
if (level >= 5) {
|
||||
const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, skillTiers);
|
||||
const selected5 = (skillUpgrades[skillId] || []).filter(id => id.includes('_l5'));
|
||||
if (upgrades5.length > 0 && selected5.length < 2) {
|
||||
return { milestone: 5, hasUpgrades: true, selectedCount: selected5.length };
|
||||
}
|
||||
}
|
||||
|
||||
// Check level 10 milestone
|
||||
if (level >= 10) {
|
||||
const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, skillTiers);
|
||||
const selected10 = (skillUpgrades[skillId] || []).filter(id => id.includes('_l10'));
|
||||
if (upgrades10.length > 0 && selected10.length < 2) {
|
||||
return { milestone: 10, hasUpgrades: true, selectedCount: selected10.length };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function SkillsTab({ store }: SkillsTabProps) {
|
||||
const [upgradeDialogSkill, setUpgradeDialogSkill] = useState<string | null>(null);
|
||||
const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5);
|
||||
const [pendingUpgradeSelections, setPendingUpgradeSelections] = useState<string[]>([]);
|
||||
|
||||
const studySpeedMult = getStudySpeedMultiplier(store.skills);
|
||||
const upgradeEffects = getUnifiedEffects(store);
|
||||
|
||||
// Get upgrade choices for dialog
|
||||
const getUpgradeChoices = () => {
|
||||
if (!upgradeDialogSkill) return { available: [] as SkillUpgradeChoice[], selected: [] as string[] };
|
||||
return store.getSkillUpgradeChoices(upgradeDialogSkill, upgradeDialogMilestone);
|
||||
};
|
||||
|
||||
const { available, selected: alreadySelected } = getUpgradeChoices();
|
||||
|
||||
// Toggle selection
|
||||
const toggleUpgrade = (upgradeId: string) => {
|
||||
const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected;
|
||||
if (currentSelections.includes(upgradeId)) {
|
||||
setPendingUpgradeSelections(currentSelections.filter(id => id !== upgradeId));
|
||||
} else if (currentSelections.length < 2) {
|
||||
setPendingUpgradeSelections([...currentSelections, upgradeId]);
|
||||
}
|
||||
};
|
||||
|
||||
// Commit selections and close
|
||||
const handleConfirm = () => {
|
||||
const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected;
|
||||
if (currentSelections.length === 2 && upgradeDialogSkill) {
|
||||
store.commitSkillUpgrades(upgradeDialogSkill, currentSelections);
|
||||
}
|
||||
setPendingUpgradeSelections([]);
|
||||
setUpgradeDialogSkill(null);
|
||||
};
|
||||
|
||||
// Cancel and close
|
||||
const handleCancel = () => {
|
||||
setPendingUpgradeSelections([]);
|
||||
setUpgradeDialogSkill(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Upgrade Selection Dialog */}
|
||||
<UpgradeDialog
|
||||
open={!!upgradeDialogSkill}
|
||||
skillId={upgradeDialogSkill}
|
||||
milestone={upgradeDialogMilestone}
|
||||
pendingSelections={pendingUpgradeSelections}
|
||||
available={available}
|
||||
alreadySelected={alreadySelected}
|
||||
onToggle={toggleUpgrade}
|
||||
onConfirm={handleConfirm}
|
||||
onCancel={handleCancel}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setPendingUpgradeSelections([]);
|
||||
setUpgradeDialogSkill(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Current Study Progress */}
|
||||
{store.currentStudyTarget && store.currentStudyTarget.type === 'skill' && (
|
||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
||||
<CardContent className="pt-4">
|
||||
<StudyProgress
|
||||
currentStudyTarget={store.currentStudyTarget}
|
||||
skills={store.skills}
|
||||
studySpeedMult={studySpeedMult}
|
||||
cancelStudy={store.cancelStudy}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{SKILL_CATEGORIES.map((cat) => {
|
||||
const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id);
|
||||
if (skillsInCat.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card key={cat.id} className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
||||
{cat.icon} {cat.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{skillsInCat.map(([id, def]) => {
|
||||
// Get tier info
|
||||
const currentTier = store.skillTiers?.[id] || 1;
|
||||
const tieredSkillId = currentTier > 1 ? `${id}_t${currentTier}` : id;
|
||||
const tierMultiplier = getTierMultiplier(tieredSkillId);
|
||||
|
||||
// Get the actual level from the tiered skill
|
||||
const level = store.skills[tieredSkillId] || store.skills[id] || 0;
|
||||
const maxed = level >= def.max;
|
||||
|
||||
// Check if studying this skill
|
||||
const isStudying = (store.currentStudyTarget?.id === id || store.currentStudyTarget?.id === tieredSkillId) && store.currentStudyTarget?.type === 'skill';
|
||||
|
||||
// Get tier name for display
|
||||
const tierDef = SKILL_EVOLUTION_PATHS[id]?.tiers.find(t => t.tier === currentTier);
|
||||
const skillDisplayName = tierDef?.name || def.name;
|
||||
|
||||
// Check prerequisites
|
||||
let prereqMet = true;
|
||||
if (def.req) {
|
||||
for (const [r, rl] of Object.entries(def.req)) {
|
||||
if ((store.skills[r] || 0) < rl) {
|
||||
prereqMet = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply skill modifiers
|
||||
const costMult = getStudyCostMultiplier(store.skills);
|
||||
const speedMult = getStudySpeedMultiplier(store.skills);
|
||||
const studyEffects = getUnifiedEffects(store);
|
||||
const effectiveSpeedMult = speedMult * studyEffects.studySpeedMultiplier;
|
||||
|
||||
// Study time scales with tier
|
||||
const tierStudyTime = def.studyTime * currentTier;
|
||||
const effectiveStudyTime = tierStudyTime / effectiveSpeedMult;
|
||||
|
||||
// Cost scales with tier
|
||||
const baseCost = def.base * (level + 1) * currentTier;
|
||||
const cost = Math.floor(baseCost * costMult);
|
||||
|
||||
// Can start studying?
|
||||
const canStudy = !maxed && prereqMet && store.rawMana >= cost && !isStudying;
|
||||
|
||||
// Check for milestone upgrades
|
||||
const milestoneInfo = hasMilestoneUpgrade(tieredSkillId, level, store.skillTiers || {}, store.skillUpgrades);
|
||||
|
||||
// Check for tier up
|
||||
const nextTierSkill = getNextTierSkill(tieredSkillId);
|
||||
const canTierUp = maxed && nextTierSkill;
|
||||
|
||||
// Get selected upgrades
|
||||
const selectedUpgrades = store.skillUpgrades[tieredSkillId] || [];
|
||||
const selectedL5 = selectedUpgrades.filter(u => u.includes('_l5'));
|
||||
const selectedL10 = selectedUpgrades.filter(u => u.includes('_l10'));
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded border gap-2 ${
|
||||
isStudying ? 'border-purple-500 bg-purple-900/20' :
|
||||
milestoneInfo ? 'border-amber-500/50 bg-amber-900/10' :
|
||||
'border-gray-700 bg-gray-800/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold text-sm">{skillDisplayName}</span>
|
||||
{currentTier > 1 && (
|
||||
<Badge className="bg-purple-600/50 text-purple-200 text-xs">Tier {currentTier} ({fmtDec(tierMultiplier, 0)}x)</Badge>
|
||||
)}
|
||||
{level > 0 && <span className="text-purple-400 text-sm">Lv.{level}</span>}
|
||||
{selectedUpgrades.length > 0 && (
|
||||
<div className="flex gap-1">
|
||||
{selectedL5.length > 0 && (
|
||||
<Badge className="bg-amber-700/50 text-amber-200 text-xs">L5: {selectedL5.length}</Badge>
|
||||
)}
|
||||
{selectedL10.length > 0 && (
|
||||
<Badge className="bg-purple-700/50 text-purple-200 text-xs">L10: {selectedL10.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 italic">{def.desc}{currentTier > 1 && ` (Tier ${currentTier}: ${fmtDec(tierMultiplier, 0)}x effect)`}</div>
|
||||
{!prereqMet && def.req && (
|
||||
<div className="text-xs text-red-400 mt-1">
|
||||
Requires: {Object.entries(def.req).map(([r, rl]) => `${SKILLS_DEF[r]?.name} Lv.${rl}`).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
<span className={effectiveSpeedMult > 1 ? 'text-green-400' : ''}>
|
||||
Study: {formatStudyTime(effectiveStudyTime)}{effectiveSpeedMult > 1 && <span className="text-xs ml-1">({Math.round(effectiveSpeedMult * 100)}% speed)</span>}
|
||||
</span>
|
||||
{' • '}
|
||||
<span className={costMult < 1 ? 'text-green-400' : ''}>
|
||||
Cost: {fmt(cost)} mana{costMult < 1 && <span className="text-xs ml-1">({Math.round(costMult * 100)}% cost)</span>}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{milestoneInfo && (
|
||||
<div className="text-xs text-amber-400 mt-1 flex items-center gap-1">
|
||||
⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-wrap sm:flex-nowrap">
|
||||
{/* Level dots */}
|
||||
<div className="flex gap-1 shrink-0">
|
||||
{Array.from({ length: def.max }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-2 h-2 rounded-full border ${
|
||||
i < level ? 'bg-purple-500 border-purple-400' :
|
||||
i === 4 || i === 9 ? 'border-amber-500' :
|
||||
'border-gray-600'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isStudying ? (
|
||||
<div className="text-xs text-purple-400">
|
||||
{formatStudyTime(store.currentStudyTarget?.progress || 0)}/{formatStudyTime(tierStudyTime)}
|
||||
</div>
|
||||
) : milestoneInfo ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
onClick={() => {
|
||||
setUpgradeDialogSkill(tieredSkillId);
|
||||
setUpgradeDialogMilestone(milestoneInfo.milestone);
|
||||
}}
|
||||
>
|
||||
Choose Upgrades
|
||||
</Button>
|
||||
) : canTierUp ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={() => store.tierUpSkill(tieredSkillId)}
|
||||
>
|
||||
⬆️ Tier Up
|
||||
</Button>
|
||||
) : maxed ? (
|
||||
<Badge className="bg-green-900/50 text-green-300">Maxed</Badge>
|
||||
) : (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={canStudy ? 'default' : 'outline'}
|
||||
disabled={!canStudy}
|
||||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||||
onClick={() => store.startStudyingSkill(tieredSkillId)}
|
||||
>
|
||||
Study ({fmt(cost)})
|
||||
</Button>
|
||||
{/* Parallel Study button */}
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
|
||||
store.currentStudyTarget &&
|
||||
!store.parallelStudyTarget &&
|
||||
store.currentStudyTarget.id !== tieredSkillId &&
|
||||
canStudy && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
||||
onClick={() => store.startParallelStudySkill(tieredSkillId)}
|
||||
>
|
||||
⚡
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Study in parallel (50% speed)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
545
src/components/game/tabs/StatsTab.tsx
Normal file
545
src/components/game/tabs/StatsTab.tsx
Normal file
@@ -0,0 +1,545 @@
|
||||
'use client';
|
||||
|
||||
import { ELEMENTS, GUARDIANS, SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { fmt, fmtDec, calcDamage } from '@/lib/game/store';
|
||||
import type { SkillUpgradeChoice, GameStore, UnifiedEffects } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Droplet, Swords, BookOpen, FlaskConical, Trophy, RotateCcw, Star } from 'lucide-react';
|
||||
|
||||
export interface StatsTabProps {
|
||||
store: GameStore;
|
||||
upgradeEffects: UnifiedEffects;
|
||||
maxMana: number;
|
||||
baseRegen: number;
|
||||
clickMana: number;
|
||||
meditationMultiplier: number;
|
||||
effectiveRegen: number;
|
||||
incursionStrength: number;
|
||||
manaCascadeBonus: number;
|
||||
studySpeedMult: number;
|
||||
studyCostMult: number;
|
||||
}
|
||||
|
||||
export function StatsTab({
|
||||
store,
|
||||
upgradeEffects,
|
||||
maxMana,
|
||||
baseRegen,
|
||||
clickMana,
|
||||
meditationMultiplier,
|
||||
effectiveRegen,
|
||||
incursionStrength,
|
||||
manaCascadeBonus,
|
||||
studySpeedMult,
|
||||
studyCostMult,
|
||||
}: StatsTabProps) {
|
||||
// Compute element max
|
||||
const elemMax = (() => {
|
||||
const ea = store.skillTiers?.elemAttune || 1;
|
||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||
const level = store.skills[tieredSkillId] || store.skills.elemAttune || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return 10 + level * 50 * tierMult + (store.prestigeUpgrades.elementalAttune || 0) * 25;
|
||||
})();
|
||||
|
||||
// Get all selected skill upgrades
|
||||
const getAllSelectedUpgrades = () => {
|
||||
const upgrades: { skillId: string; upgrade: SkillUpgradeChoice }[] = [];
|
||||
for (const [skillId, selectedIds] of Object.entries(store.skillUpgrades)) {
|
||||
const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||
const path = SKILL_EVOLUTION_PATHS[baseSkillId];
|
||||
if (!path) continue;
|
||||
for (const tier of path.tiers) {
|
||||
if (tier.skillId === skillId) {
|
||||
for (const upgradeId of selectedIds) {
|
||||
const upgrade = tier.upgrades.find(u => u.id === upgradeId);
|
||||
if (upgrade) {
|
||||
upgrades.push({ skillId, upgrade });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return upgrades;
|
||||
};
|
||||
|
||||
const selectedUpgrades = getAllSelectedUpgrades();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Mana Stats */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-blue-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Droplet className="w-4 h-4" />
|
||||
Mana Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Base Max Mana:</span>
|
||||
<span className="text-gray-200">100</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Well Bonus:</span>
|
||||
<span className="text-blue-300">
|
||||
{(() => {
|
||||
const mw = store.skillTiers?.manaWell || 1;
|
||||
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
|
||||
const level = store.skills[tieredSkillId] || store.skills.manaWell || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${fmt(level * 100 * tierMult)} (${level} lvl × 100 × ${tierMult}x tier)`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Prestige Mana Well:</span>
|
||||
<span className="text-blue-300">+{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}</span>
|
||||
</div>
|
||||
{upgradeEffects.maxManaBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Upgrade Mana Bonus:</span>
|
||||
<span className="text-amber-300">+{fmt(upgradeEffects.maxManaBonus)}</span>
|
||||
</div>
|
||||
)}
|
||||
{upgradeEffects.maxManaMultiplier > 1 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Upgrade Mana Multiplier:</span>
|
||||
<span className="text-amber-300">×{fmtDec(upgradeEffects.maxManaMultiplier, 2)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||||
<span className="text-gray-300">Total Max Mana:</span>
|
||||
<span className="text-blue-400">{fmt(maxMana)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Base Regen:</span>
|
||||
<span className="text-gray-200">2/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Flow Bonus:</span>
|
||||
<span className="text-blue-300">
|
||||
{(() => {
|
||||
const mf = store.skillTiers?.manaFlow || 1;
|
||||
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
|
||||
const level = store.skills[tieredSkillId] || store.skills.manaFlow || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${fmtDec(level * 1 * tierMult)}/hr (${level} lvl × 1 × ${tierMult}x tier)`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Spring Bonus:</span>
|
||||
<span className="text-blue-300">+{(store.skills.manaSpring || 0) * 2}/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Prestige Mana Flow:</span>
|
||||
<span className="text-blue-300">+{fmtDec((store.prestigeUpgrades.manaFlow || 0) * 0.5)}/hr</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Temporal Echo:</span>
|
||||
<span className="text-blue-300">×{fmtDec(1 + (store.prestigeUpgrades.temporalEcho || 0) * 0.1, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||||
<span className="text-gray-300">Base Regen:</span>
|
||||
<span className="text-blue-400">{fmtDec(baseRegen, 2)}/hr</span>
|
||||
</div>
|
||||
{upgradeEffects.regenBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Upgrade Regen Bonus:</span>
|
||||
<span className="text-amber-300">+{fmtDec(upgradeEffects.regenBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{upgradeEffects.permanentRegenBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Permanent Regen Bonus:</span>
|
||||
<span className="text-amber-300">+{fmtDec(upgradeEffects.permanentRegenBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{upgradeEffects.regenMultiplier > 1 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-amber-400">Upgrade Regen Multiplier:</span>
|
||||
<span className="text-amber-300">×{fmtDec(upgradeEffects.regenMultiplier, 2)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-gray-700 my-3" />
|
||||
{upgradeEffects.activeUpgrades.length > 0 && (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<span className="text-xs text-amber-400 game-panel-title">Active Skill Upgrades</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-3">
|
||||
{upgradeEffects.activeUpgrades.map((upgrade, idx) => (
|
||||
<div key={idx} className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||||
<span className="text-gray-300">{upgrade.name}</span>
|
||||
<span className="text-gray-400">{upgrade.desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Separator className="bg-gray-700 my-3" />
|
||||
</>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Click Mana Value:</span>
|
||||
<span className="text-purple-300">+{clickMana}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Tap Bonus:</span>
|
||||
<span className="text-purple-300">+{store.skills.manaTap || 0}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Surge Bonus:</span>
|
||||
<span className="text-purple-300">+{(store.skills.manaSurge || 0) * 3}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Mana Overflow:</span>
|
||||
<span className="text-purple-300">×{fmtDec(1 + (store.skills.manaOverflow || 0) * 0.25, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Meditation Multiplier:</span>
|
||||
<span className={`font-semibold ${meditationMultiplier > 1.5 ? 'text-purple-400' : 'text-gray-300'}`}>
|
||||
{fmtDec(meditationMultiplier, 2)}x
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Effective Regen:</span>
|
||||
<span className="text-green-400 font-semibold">{fmtDec(effectiveRegen, 2)}/hr</span>
|
||||
</div>
|
||||
{incursionStrength > 0 && !hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM) && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-red-400">Incursion Penalty:</span>
|
||||
<span className="text-red-400">-{Math.round(incursionStrength * 100)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM) && incursionStrength > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-400">Steady Stream:</span>
|
||||
<span className="text-green-400">Immune to incursion</span>
|
||||
</div>
|
||||
)}
|
||||
{manaCascadeBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Cascade Bonus:</span>
|
||||
<span className="text-cyan-400">+{fmtDec(manaCascadeBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_TORRENT) && store.rawMana > maxMana * 0.75 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Torrent:</span>
|
||||
<span className="text-cyan-400">+50% regen (high mana)</span>
|
||||
</div>
|
||||
)}
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.DESPERATE_WELLS) && store.rawMana < maxMana * 0.25 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Desperate Wells:</span>
|
||||
<span className="text-cyan-400">+50% regen (low mana)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Combat Stats */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-red-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Swords className="w-4 h-4" />
|
||||
Combat Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Combat Training Bonus:</span>
|
||||
<span className="text-red-300">+{(store.skills.combatTrain || 0) * 5}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Arcane Fury Multiplier:</span>
|
||||
<span className="text-red-300">×{fmtDec(1 + (store.skills.arcaneFury || 0) * 0.1, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Elemental Mastery:</span>
|
||||
<span className="text-red-300">×{fmtDec(1 + (store.skills.elementalMastery || 0) * 0.15, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Guardian Bane:</span>
|
||||
<span className="text-red-300">×{fmtDec(1 + (store.skills.guardianBane || 0) * 0.2, 2)} (vs guardians)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Critical Hit Chance:</span>
|
||||
<span className="text-amber-300">{((store.skills.precision || 0) * 5)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Critical Multiplier:</span>
|
||||
<span className="text-amber-300">1.5x</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Spell Echo Chance:</span>
|
||||
<span className="text-amber-300">{((store.skills.spellEcho || 0) * 10)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Pact Multiplier:</span>
|
||||
<span className="text-amber-300">×{fmtDec(store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Study Stats */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Study Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Study Speed:</span>
|
||||
<span className="text-purple-300">×{fmtDec(studySpeedMult, 2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Quick Learner Bonus:</span>
|
||||
<span className="text-purple-300">+{((store.skills.quickLearner || 0) * 10)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Study Cost:</span>
|
||||
<span className="text-purple-300">{Math.round(studyCostMult * 100)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Focused Mind Bonus:</span>
|
||||
<span className="text-purple-300">-{((store.skills.focusedMind || 0) * 5)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Progress Retention:</span>
|
||||
<span className="text-purple-300">{Math.round((1 + (store.skills.knowledgeRetention || 0) * 0.2) * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Element Stats */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-green-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<FlaskConical className="w-4 h-4" />
|
||||
Element Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Element Capacity:</span>
|
||||
<span className="text-green-300">{elemMax}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Elem. Attunement Bonus:</span>
|
||||
<span className="text-green-300">
|
||||
{(() => {
|
||||
const ea = store.skillTiers?.elemAttune || 1;
|
||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||
const level = store.skills[tieredSkillId] || store.skills.elemAttune || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${level * 50 * tierMult}`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Prestige Attunement:</span>
|
||||
<span className="text-green-300">+{(store.prestigeUpgrades.elementalAttune || 0) * 25}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Unlocked Elements:</span>
|
||||
<span className="text-green-300">{Object.values(store.elements).filter(e => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Elem. Crafting Bonus:</span>
|
||||
<span className="text-green-300">×{fmtDec(1 + (store.skills.elemCrafting || 0) * 0.25, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-gray-700 my-3" />
|
||||
<div className="text-xs text-gray-400 mb-2">Elemental Mana Pools:</div>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
||||
{Object.entries(store.elements)
|
||||
.filter(([, state]) => state.unlocked)
|
||||
.map(([id, state]) => {
|
||||
const def = ELEMENTS[id];
|
||||
return (
|
||||
<div key={id} className="p-2 rounded border border-gray-700 bg-gray-800/50 text-center">
|
||||
<div className="text-lg">{def?.sym}</div>
|
||||
<div className="text-xs text-gray-400">{state.current}/{state.max}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Active Upgrades */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Star className="w-4 h-4" />
|
||||
Active Skill Upgrades ({selectedUpgrades.length})
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{selectedUpgrades.length === 0 ? (
|
||||
<div className="text-gray-500 text-sm">No skill upgrades selected yet. Level skills to 5 or 10 to choose upgrades.</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
{selectedUpgrades.map(({ skillId, upgrade }) => (
|
||||
<div key={upgrade.id} className="p-2 rounded border border-amber-600/30 bg-amber-900/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-amber-300 text-sm font-semibold">{upgrade.name}</span>
|
||||
<Badge variant="outline" className="text-xs text-gray-400">
|
||||
{SKILLS_DEF[skillId]?.name || skillId}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||||
{upgrade.effect.type === 'multiplier' && (
|
||||
<div className="text-xs text-green-400 mt-1">
|
||||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'bonus' && (
|
||||
<div className="text-xs text-blue-400 mt-1">
|
||||
+{upgrade.effect.value} {upgrade.effect.stat}
|
||||
</div>
|
||||
)}
|
||||
{upgrade.effect.type === 'special' && (
|
||||
<div className="text-xs text-cyan-400 mt-1">
|
||||
⚡ {upgrade.effect.specialDesc || 'Special effect active'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pact Bonuses */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Trophy className="w-4 h-4" />
|
||||
Signed Pacts ({store.signedPacts.length}/10)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{store.signedPacts.length === 0 ? (
|
||||
<div className="text-gray-500 text-sm">No pacts signed yet. Defeat guardians to earn pacts.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{store.signedPacts.map((floor) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return null;
|
||||
return (
|
||||
<div
|
||||
key={floor}
|
||||
className="flex items-center justify-between p-2 rounded border"
|
||||
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
||||
{guardian.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Floor {floor}</div>
|
||||
</div>
|
||||
<Badge className="bg-amber-900/50 text-amber-300">
|
||||
{guardian.pact}x multiplier
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2 mt-2">
|
||||
<span className="text-gray-300">Combined Pact Multiplier:</span>
|
||||
<span className="text-amber-400">×{fmtDec(store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Loop Stats */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Loop Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
|
||||
<div className="text-xs text-gray-400">Loops Completed</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
|
||||
<div className="text-xs text-gray-400">Current Insight</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
|
||||
<div className="text-xs text-gray-400">Total Insight</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-green-400 game-mono">{store.maxFloorReached}</div>
|
||||
<div className="text-xs text-gray-400">Max Floor</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-gray-700 my-3" />
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(store.spells).filter(s => s.learned).length}</div>
|
||||
<div className="text-xs text-gray-400">Spells Learned</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(store.skills).reduce((a, b) => a + b, 0)}</div>
|
||||
<div className="text-xs text-gray-400">Total Skill Levels</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-gray-300 game-mono">{fmt(store.totalManaGathered)}</div>
|
||||
<div className="text-xs text-gray-400">Total Mana Gathered</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-gray-300 game-mono">{store.memorySlots}</div>
|
||||
<div className="text-xs text-gray-400">Memory Slots</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,3 +5,5 @@ export { CraftingTab } from './CraftingTab';
|
||||
export { SpireTab } from './SpireTab';
|
||||
export { SpellsTab } from './SpellsTab';
|
||||
export { LabTab } from './LabTab';
|
||||
export { SkillsTab } from './SkillsTab';
|
||||
export { StatsTab } from './StatsTab';
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
MAX_DAY,
|
||||
INCURSION_START_DAY,
|
||||
ELEMENT_OPPOSITES,
|
||||
ELEMENTS,
|
||||
TICK_MS,
|
||||
} from './constants';
|
||||
import type { ComputedEffects } from './upgrade-effects';
|
||||
import { getUnifiedEffects, type UnifiedEffects } from './effects';
|
||||
@@ -395,3 +397,95 @@ export function deductSpellCost(
|
||||
|
||||
return { rawMana, elements: newElements };
|
||||
}
|
||||
|
||||
// ─── Damage Breakdown Helper ───────────────────────────────────────────────────
|
||||
|
||||
export interface DamageBreakdown {
|
||||
base: number;
|
||||
combatTrainBonus: number;
|
||||
arcaneFuryMult: number;
|
||||
elemMasteryMult: number;
|
||||
guardianBaneMult: number;
|
||||
pactMult: number;
|
||||
precisionChance: number;
|
||||
elemBonus: number;
|
||||
elemBonusText: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function getDamageBreakdown(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts'>,
|
||||
activeSpellId: string,
|
||||
floorElem: string,
|
||||
isGuardianFloor: boolean
|
||||
): DamageBreakdown | null {
|
||||
const spell = SPELLS_DEF[activeSpellId];
|
||||
if (!spell) return null;
|
||||
|
||||
const baseDmg = spell.dmg;
|
||||
const combatTrainBonus = (state.skills.combatTrain || 0) * 5;
|
||||
const arcaneFuryMult = 1 + (state.skills.arcaneFury || 0) * 0.1;
|
||||
const elemMasteryMult = 1 + (state.skills.elementalMastery || 0) * 0.15;
|
||||
const guardianBaneMult = isGuardianFloor ? (1 + (state.skills.guardianBane || 0) * 0.2) : 1;
|
||||
const pactMult = state.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1);
|
||||
const precisionChance = (state.skills.precision || 0) * 0.05;
|
||||
|
||||
// Elemental bonus
|
||||
let elemBonus = 1.0;
|
||||
let elemBonusText = '';
|
||||
if (spell.elem !== 'raw' && floorElem) {
|
||||
if (spell.elem === floorElem) {
|
||||
elemBonus = 1.25;
|
||||
elemBonusText = '+25% same element';
|
||||
} else if (ELEMENTS[spell.elem] && ELEMENT_OPPOSITES[floorElem] === spell.elem) {
|
||||
elemBonus = 1.5;
|
||||
elemBonusText = '+50% super effective';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
base: baseDmg,
|
||||
combatTrainBonus,
|
||||
arcaneFuryMult,
|
||||
elemMasteryMult,
|
||||
guardianBaneMult,
|
||||
pactMult,
|
||||
precisionChance,
|
||||
elemBonus,
|
||||
elemBonusText,
|
||||
total: calcDamage(state, activeSpellId, floorElem)
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Total DPS Calculation ─────────────────────────────────────────────────────
|
||||
|
||||
export function getTotalDPS(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts' | 'equippedInstances' | 'equipmentInstances'>,
|
||||
upgradeEffects: { attackSpeedMultiplier: number },
|
||||
floorElem: string
|
||||
): number {
|
||||
const quickCastBonus = 1 + (state.skills.quickCast || 0) * 0.05;
|
||||
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
|
||||
const castsPerSecondMult = HOURS_PER_TICK / (TICK_MS / 1000);
|
||||
|
||||
const activeEquipmentSpells = getActiveEquipmentSpells(
|
||||
state.equippedInstances,
|
||||
state.equipmentInstances
|
||||
);
|
||||
|
||||
let totalDPS = 0;
|
||||
|
||||
for (const { spellId } of activeEquipmentSpells) {
|
||||
const spell = SPELLS_DEF[spellId];
|
||||
if (!spell) continue;
|
||||
|
||||
const spellCastSpeed = spell.castSpeed || 1;
|
||||
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
|
||||
const damagePerCast = calcDamage(state, spellId, floorElem);
|
||||
const castsPerSecond = totalCastSpeed * castsPerSecondMult;
|
||||
|
||||
totalDPS += damagePerCast * castsPerSecond;
|
||||
}
|
||||
|
||||
return totalDPS;
|
||||
}
|
||||
|
||||
@@ -835,3 +835,28 @@ export const ELEMENT_OPPOSITES: Record<string, string> = {
|
||||
light: 'dark', dark: 'light',
|
||||
life: 'death', death: 'life',
|
||||
};
|
||||
|
||||
// ─── Element Icon Mapping (Lucide Icons) ──────────────────────────────────────
|
||||
// Note: These are string identifiers for dynamic icon loading
|
||||
// The actual Lucide icons are imported in the components
|
||||
export const ELEMENT_ICON_NAMES: Record<string, string> = {
|
||||
fire: 'Flame',
|
||||
water: 'Droplet',
|
||||
air: 'Wind',
|
||||
earth: 'Mountain',
|
||||
light: 'Sun',
|
||||
dark: 'Moon',
|
||||
life: 'Leaf',
|
||||
death: 'Skull',
|
||||
mental: 'Brain',
|
||||
transference: 'Link',
|
||||
force: 'Wind',
|
||||
blood: 'Droplets',
|
||||
metal: 'Target',
|
||||
wood: 'TreeDeciduous',
|
||||
sand: 'Hourglass',
|
||||
crystal: 'Gem',
|
||||
stellar: 'Star',
|
||||
void: 'CircleDot',
|
||||
raw: 'Circle',
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
BASE_UNLOCKED_EFFECTS,
|
||||
ENCHANTING_UNLOCK_EFFECTS,
|
||||
} from './constants';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from './upgrade-effects';
|
||||
import { hasSpecial, SPECIAL_EFFECTS, computeDynamicRegen } from './upgrade-effects';
|
||||
import { getUnifiedEffects } from './effects';
|
||||
import { SKILL_EVOLUTION_PATHS } from './skill-evolution';
|
||||
import {
|
||||
@@ -72,6 +72,8 @@ import {
|
||||
getIncursionStrength,
|
||||
canAffordSpellCost,
|
||||
deductSpellCost,
|
||||
getTotalDPS,
|
||||
getDamageBreakdown,
|
||||
} from './computed-stats';
|
||||
|
||||
// Re-export formatting functions and computed stats for backward compatibility
|
||||
@@ -87,6 +89,9 @@ export {
|
||||
getIncursionStrength,
|
||||
canAffordSpellCost,
|
||||
getFloorMaxHP,
|
||||
getActiveEquipmentSpells,
|
||||
getTotalDPS,
|
||||
getDamageBreakdown,
|
||||
};
|
||||
|
||||
// ─── Local Helper Functions ────────────────────────────────────────────────────
|
||||
@@ -201,6 +206,9 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
skillUpgrades: overrides.skillUpgrades || {},
|
||||
skillTiers: overrides.skillTiers || {},
|
||||
parallelStudyTarget: null,
|
||||
studyStartedAt: null,
|
||||
consecutiveStudyHours: 0,
|
||||
lastStudyCost: 0,
|
||||
|
||||
// New equipment system
|
||||
equippedInstances: startingEquipment.equippedInstances,
|
||||
@@ -251,6 +259,7 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
elementChain: [],
|
||||
},
|
||||
totalTicks: 0,
|
||||
consecutiveHits: 0,
|
||||
|
||||
// Loot System
|
||||
lootInventory: {
|
||||
@@ -410,8 +419,15 @@ export const useGameStore = create<GameStore>()(
|
||||
meditateTicks = 0;
|
||||
}
|
||||
|
||||
// Calculate effective regen with incursion and meditation
|
||||
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
|
||||
// Calculate effective regen with dynamic special effects
|
||||
// computeDynamicRegen handles: Mana Cascade, Mana Torrent, Desperate Wells, Steady Stream
|
||||
let effectiveRegen = computeDynamicRegen(
|
||||
effects,
|
||||
baseRegen,
|
||||
maxMana,
|
||||
state.rawMana,
|
||||
incursionStrength
|
||||
) * meditationMultiplier;
|
||||
|
||||
// Mana regeneration
|
||||
let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
||||
@@ -890,11 +906,28 @@ export const useGameStore = create<GameStore>()(
|
||||
const overflowBonus = 1 + (state.skills.manaOverflow || 0) * 0.25;
|
||||
cm = Math.floor(cm * overflowBonus);
|
||||
|
||||
// MANA_ECHO: 10% chance to gain double mana from clicks
|
||||
let echoTriggered = false;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_ECHO) && Math.random() < 0.1) {
|
||||
cm *= 2;
|
||||
echoTriggered = true;
|
||||
}
|
||||
|
||||
const max = computeMaxMana(state, effects);
|
||||
set({
|
||||
rawMana: Math.min(state.rawMana + cm, max),
|
||||
totalManaGathered: state.totalManaGathered + cm,
|
||||
});
|
||||
const newRawMana = Math.min(state.rawMana + cm, max);
|
||||
|
||||
if (echoTriggered) {
|
||||
set({
|
||||
rawMana: newRawMana,
|
||||
totalManaGathered: state.totalManaGathered + cm,
|
||||
log: [`✨ Mana Echo! Gained ${cm} mana (doubled)!`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
} else {
|
||||
set({
|
||||
rawMana: newRawMana,
|
||||
totalManaGathered: state.totalManaGathered + cm,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setAction: (action: GameAction) => {
|
||||
@@ -1015,6 +1048,11 @@ export const useGameStore = create<GameStore>()(
|
||||
const insightGained = state.loopInsight || calcInsight(state);
|
||||
const total = state.insight + insightGained;
|
||||
|
||||
// Check for EMERGENCY_RESERVE before creating new state
|
||||
const effects = getUnifiedEffects(state);
|
||||
const maxMana = computeMaxMana(state, effects);
|
||||
const hasEmergencyReserve = hasSpecial(effects, SPECIAL_EFFECTS.EMERGENCY_RESERVE);
|
||||
|
||||
// Keep some spells through temporal memory
|
||||
let spellsToKeep: string[] = [];
|
||||
if (state.skills.temporalMemory) {
|
||||
@@ -1040,6 +1078,13 @@ export const useGameStore = create<GameStore>()(
|
||||
});
|
||||
}
|
||||
|
||||
// EMERGENCY_RESERVE: Keep 10% of max mana when starting new loop
|
||||
if (hasEmergencyReserve) {
|
||||
const reserveMana = Math.floor(maxMana * 0.1);
|
||||
newState.rawMana = reserveMana;
|
||||
newState.log = [`💫 Emergency Reserve preserved ${reserveMana} mana!`, ...newState.log.slice(0, 49)];
|
||||
}
|
||||
|
||||
set(newState);
|
||||
},
|
||||
|
||||
|
||||
@@ -472,6 +472,11 @@ export interface GameState {
|
||||
// Parallel Study Target (for Parallel Mind milestone upgrade)
|
||||
parallelStudyTarget: StudyTarget | null;
|
||||
|
||||
// Study tracking for special effects
|
||||
studyStartedAt: number | null; // Tick when study started (for STUDY_RUSH)
|
||||
consecutiveStudyHours: number; // Consecutive hours studying (for STUDY_MOMENTUM)
|
||||
lastStudyCost: number; // Cost of starting current study (for STUDY_REFUND)
|
||||
|
||||
// Prestige
|
||||
insight: number;
|
||||
totalInsight: number;
|
||||
@@ -486,6 +491,7 @@ export interface GameState {
|
||||
// Combo System
|
||||
combo: ComboState;
|
||||
totalTicks: number; // Total ticks this loop (for combo timing)
|
||||
consecutiveHits: number; // Consecutive hits for BATTLE_FURY tracking
|
||||
|
||||
// Loot System
|
||||
lootInventory: LootInventory;
|
||||
|
||||
242
worklog.md
242
worklog.md
@@ -440,3 +440,245 @@ Create two new store slices to reduce the size of `/home/z/my-project/src/lib/ga
|
||||
- Reduced store.ts size by extracting ~100 lines of action implementations
|
||||
- Better code organization with navigation and study logic in dedicated modules
|
||||
- Easier to maintain and extend individual features
|
||||
|
||||
---
|
||||
## Task ID: 1 - Documentation Update
|
||||
### Work Task
|
||||
Update the AGENTS.md file to reflect the current refactored codebase structure, including new file structure, component organization, file size reductions, and slice pattern documentation.
|
||||
|
||||
### Work Summary
|
||||
**Updated Sections in AGENTS.md:**
|
||||
|
||||
1. **Directory Structure Section**:
|
||||
- Updated `page.tsx` line count (~1700 lines)
|
||||
- Added new component files: ActionButtons.tsx, CalendarDisplay.tsx, CraftingProgress.tsx, StudyProgress.tsx, ManaDisplay.tsx, TimeDisplay.tsx
|
||||
- Added tabs/index.ts for cleaner exports
|
||||
- Added new lib/game files: computed-stats.ts, navigation-slice.ts, study-slice.ts, familiar-slice.ts
|
||||
- Updated store.ts line count (~1650 lines)
|
||||
|
||||
2. **State Management Section**:
|
||||
- Added slice pattern documentation showing how the store is organized
|
||||
- Listed all store slices with their purposes
|
||||
- Added computed-stats.ts function list
|
||||
- Maintained GameState interface for reference
|
||||
|
||||
3. **File Size Guidelines Section**:
|
||||
- Added table showing current file sizes after refactoring
|
||||
- Documented size reductions: store.ts (23% reduction), page.tsx (34% reduction)
|
||||
- Added slice extraction guidelines
|
||||
|
||||
4. **New "Slice Pattern for Store Organization" Section**:
|
||||
- Added complete documentation on creating new slices
|
||||
- Provided code examples for slice creation
|
||||
- Listed all existing slices with their purposes and files
|
||||
- Explained integration with main store
|
||||
|
||||
**Key Changes Documented:**
|
||||
- Store slice pattern with navigation, study, crafting, and familiar slices
|
||||
- Computed stats extraction to dedicated module
|
||||
- UI component extraction (6 new components)
|
||||
- Tab component organization with barrel exports
|
||||
- File size reductions from refactoring efforts
|
||||
|
||||
---
|
||||
## Task ID: 2 - README.md Creation
|
||||
### Work Task
|
||||
Create a comprehensive README.md file for the Mana Loop project, including project overview, features, tech stack, getting started instructions, project structure, game systems overview, contributing guidelines, and license.
|
||||
|
||||
### Work Summary
|
||||
**Created New File:** `README.md`
|
||||
|
||||
**Sections Included:**
|
||||
|
||||
1. **Project Title & Description**
|
||||
- Mana Loop branding with tagline
|
||||
- Overview of the game as an incremental/idle game about climbing a magical spire
|
||||
- Brief description of the 5-step game loop (Gather → Study → Climb → Craft → Prestige)
|
||||
|
||||
2. **Features**
|
||||
- Mana Gathering & Management (click-based, elemental mana, meditation)
|
||||
- Skill Progression with Tier Evolution (20+ skills, 5-tier evolution, milestone upgrades)
|
||||
- Equipment Crafting & Enchanting (3-stage process, capacity system, disenchanting)
|
||||
- Combat System (cast speed-based, multi-spell support, elemental bonuses)
|
||||
- Familiar System (collect, train, evolve companions)
|
||||
- Floor Navigation & Guardian Battles (procedural floors, guardians, pacts)
|
||||
- Prestige System (Insight, permanent bonuses, persistent pacts)
|
||||
|
||||
3. **Tech Stack**
|
||||
- Next.js 16 with App Router
|
||||
- TypeScript 5
|
||||
- Tailwind CSS 4 with shadcn/ui
|
||||
- Zustand for state management
|
||||
- Prisma ORM with SQLite
|
||||
- Bun runtime
|
||||
|
||||
4. **Getting Started**
|
||||
- Prerequisites (Node.js 18+ or Bun)
|
||||
- Installation commands (clone, install, db:push)
|
||||
- Development server instructions
|
||||
- Additional commands (lint, build, start)
|
||||
|
||||
5. **Project Structure**
|
||||
- Complete directory tree with file descriptions
|
||||
- Link to AGENTS.md for detailed architecture
|
||||
|
||||
6. **Game Systems Overview**
|
||||
- Brief descriptions of each major system
|
||||
- Links to relevant source files for each system
|
||||
|
||||
7. **Contributing**
|
||||
- Development workflow guidelines
|
||||
- Code style requirements
|
||||
- Reference to AGENTS.md for detailed patterns
|
||||
|
||||
8. **License**
|
||||
- MIT License with full text
|
||||
|
||||
**Documentation Quality:**
|
||||
- Well-formatted with markdown tables and code blocks
|
||||
- Comprehensive coverage of all game systems
|
||||
- Clear navigation between sections
|
||||
- Links to AGENTS.md for technical details
|
||||
- Professional presentation suitable for open-source projects
|
||||
|
||||
---
|
||||
## Task ID: 4 - Codebase Audit
|
||||
### Work Task
|
||||
Audit the codebase for unimplemented effects, upgrades, and missing functionality. Check upgrade-effects.ts, enchantment-effects.ts, constants.ts, skill-evolution.ts, and store.ts for TODOs and incomplete implementations.
|
||||
|
||||
### Work Summary
|
||||
**Created Audit Report:** `AUDIT_REPORT.md`
|
||||
|
||||
**Key Findings:**
|
||||
|
||||
1. **Special Effects Implementation Status:**
|
||||
- 32 special effects defined in SPECIAL_EFFECTS constant
|
||||
- Only 2 fully implemented (OVERPOWER, BERSERKER)
|
||||
- 6 partially implemented (functions exist but not called)
|
||||
- 24 not implemented at all
|
||||
|
||||
2. **Critical Issue - Dynamic Functions Not Called:**
|
||||
- `computeDynamicRegen()` in upgrade-effects.ts exists but is NOT called from store.ts
|
||||
- `computeDynamicDamage()` in upgrade-effects.ts exists but is NOT called from store.ts
|
||||
- These functions implement Mana Cascade, Mana Torrent, Desperate Wells, Steady Stream, Battle Fury
|
||||
|
||||
3. **Enchantment Special Effects:**
|
||||
- Spell, bonus, and multiplier effects are properly applied
|
||||
- Special effects (spellEcho10, lifesteal5) are tracked in Set but never used in combat logic
|
||||
|
||||
4. **Missing Combat Effects:**
|
||||
- BATTLE_FURY: Requires consecutiveHits tracking (not in GameState)
|
||||
- COMBO_MASTER: Requires hit counter
|
||||
- ADRENALINE_RUSH: Should restore 5% mana on kill
|
||||
- ARMOR_PIERCE: Floor defense not implemented
|
||||
|
||||
5. **Missing Study Effects:**
|
||||
- MENTAL_CLARITY: +10% speed when mana > 75%
|
||||
- STUDY_RUSH: First hour 2x speed
|
||||
- STUDY_REFUND: 25% mana back on completion
|
||||
- KNOWLEDGE_ECHO: 10% instant study chance
|
||||
|
||||
6. **Missing Loop Effects:**
|
||||
- MANA_ECHO: 10% double click mana - not in gatherMana()
|
||||
- EMERGENCY_RESERVE: Keep 10% mana on new loop - not in startNewLoop()
|
||||
|
||||
7. **Balance Concerns Identified:**
|
||||
- manaThreshold upgrade is net negative early game
|
||||
- scrollCrafting skill exists but scroll system not implemented
|
||||
- fieldRepair skill exists but repair system not implemented
|
||||
- Research skills unlock effects that don't work
|
||||
|
||||
8. **200+ Special Effects in skill-evolution.ts:**
|
||||
- Across 5 tiers of evolution paths
|
||||
- Most tier 2-5 effects are not implemented
|
||||
- Victory conditions defined but not coded
|
||||
|
||||
**Files Analyzed:**
|
||||
- `src/lib/game/upgrade-effects.ts` - SPECIAL_EFFECTS constant and dynamic functions
|
||||
- `src/lib/game/data/enchantment-effects.ts` - Enchantment effect definitions
|
||||
- `src/lib/game/constants.ts` - SKILLS_DEF and skill effects
|
||||
- `src/lib/game/skill-evolution.ts` - 200+ special effect definitions across tiers
|
||||
- `src/lib/game/store.ts` - Game tick and action logic
|
||||
- `src/lib/game/computed-stats.ts` - Computed stat functions
|
||||
- `src/lib/game/effects.ts` - Unified effects system
|
||||
|
||||
**Recommendations Provided:**
|
||||
- Priority 1: Call dynamic regen/damage functions, implement core effects
|
||||
- Priority 2: Add hit tracking state, implement combat specials
|
||||
- Priority 3: Implement study-related effects
|
||||
- Priority 4: Implement or remove unused skills (scrollCrafting, fieldRepair)
|
||||
|
||||
---
|
||||
## Task ID: 3 - Further Component Extraction
|
||||
### Work Task
|
||||
Continue refactoring `/home/z/my-project/src/app/page.tsx` to reduce its size further (was ~1695 lines). Extract remaining large render functions to dedicated components.
|
||||
|
||||
### Work Summary
|
||||
|
||||
**Components Created:**
|
||||
|
||||
1. **`UpgradeDialog.tsx`** (115 lines):
|
||||
- Extracted from inline `renderUpgradeDialog()`
|
||||
- Dialog for selecting skill upgrades at milestones (level 5 and 10)
|
||||
- Proper props interface with callbacks for toggle, confirm, cancel
|
||||
- Supports selecting 2 upgrades per milestone
|
||||
|
||||
2. **`SkillsTab.tsx`** (338 lines):
|
||||
- Extracted from inline `renderSkillsTab()`
|
||||
- Complete skills display with:
|
||||
- Study progress display
|
||||
- Category-organized skill list
|
||||
- Tier evolution display
|
||||
- Milestone upgrade selection
|
||||
- Tier-up functionality
|
||||
- Parallel study support (for Parallel Mind upgrade)
|
||||
- Includes internal `hasMilestoneUpgrade()` helper
|
||||
|
||||
3. **`StatsTab.tsx`** (545 lines):
|
||||
- Extracted from inline `renderStatsTab()`
|
||||
- Comprehensive stats overview with:
|
||||
- Mana stats (max mana, regen, click mana)
|
||||
- Combat stats (damage bonuses, crit chance)
|
||||
- Study stats (speed, cost, retention)
|
||||
- Element stats (capacity, unlocked elements)
|
||||
- Active skill upgrades display
|
||||
- Signed pacts display
|
||||
- Loop stats summary
|
||||
|
||||
**Functions Moved to computed-stats.ts:**
|
||||
|
||||
1. **`getDamageBreakdown()`** - Computes detailed damage breakdown for display
|
||||
- Returns base damage, bonuses, multipliers, and total
|
||||
- Includes elemental bonus calculation
|
||||
|
||||
2. **`getTotalDPS()`** - Computes total DPS from all active equipment spells
|
||||
- Iterates through all equipped spells
|
||||
- Sums DPS based on cast speed and damage
|
||||
|
||||
**Constants Moved:**
|
||||
|
||||
- **`ELEMENT_ICON_NAMES`** - Added to constants.ts
|
||||
- Maps element IDs to Lucide icon names for dynamic icon loading
|
||||
|
||||
**Exports Updated:**
|
||||
|
||||
- `store.ts`: Added exports for `getActiveEquipmentSpells`, `getTotalDPS`, `getDamageBreakdown`
|
||||
- `tabs/index.ts`: Added exports for `SkillsTab`, `StatsTab`
|
||||
- `game/index.ts`: Added export for `UpgradeDialog`
|
||||
|
||||
**File Size Results:**
|
||||
|
||||
| File | Before | After | Reduction |
|
||||
|------|--------|-------|-----------|
|
||||
| page.tsx | ~1695 lines | 434 lines | **74% reduction** |
|
||||
| SkillsTab.tsx | - | 338 lines | New |
|
||||
| StatsTab.tsx | - | 545 lines | New |
|
||||
| UpgradeDialog.tsx | - | 115 lines | New |
|
||||
| computed-stats.ts | ~398 lines | 491 lines | +93 lines |
|
||||
|
||||
**Results:**
|
||||
- All lint checks pass
|
||||
- Functionality preserved - all features working as before
|
||||
- page.tsx now well under the 1000 line target (434 lines)
|
||||
- Better code organization with skills, stats, and upgrade logic in dedicated modules
|
||||
- Easier to test and maintain individual features
|
||||
|
||||
Reference in New Issue
Block a user