Compare commits
11 Commits
d6b85d6367
...
f2d046c9e2
| Author | SHA1 | Date | |
|---|---|---|---|
| f2d046c9e2 | |||
| 77f181b4a1 | |||
| 8d1d328c3f | |||
| 0f0b800e60 | |||
| c8cabf3e4b | |||
| edfc6f11c0 | |||
| 75a43c7209 | |||
| 132a4e6a72 | |||
| 6e3b867e7d | |||
| 7d1bfbe4dc | |||
| eea5ed1585 |
+49
-26
@@ -1,68 +1,91 @@
|
||||
# Phase 3: Refactor Large Files - Progress
|
||||
# Phase 3: Refactor Large Files - Progress #
|
||||
|
||||
## Completed Refactorings (All Committed & Pushed)
|
||||
## Completed Refactorings (All Committed & Pushed!)
|
||||
|
||||
### 1. `types.ts` (516 lines) ✅
|
||||
- **Commit**: `eb81ccb Phase 3: Split types.ts into domain-specific files`
|
||||
- **Result**: Split into `types/elements.ts`, `types/attunements.ts`, `types/spells.ts`, `types/skills.ts`, `types/equipment.ts`, `types/game.ts`, `types/index.ts`
|
||||
- **Result**: Split into domain-specific files
|
||||
- **Build**: ✅ Passes
|
||||
|
||||
### 2. `constants.ts` (1436 lines) ✅
|
||||
- **Commit**: `f8520e1 Phase 3: Split constants.ts into domain-specific files`
|
||||
- **Result**: Split into `constants/elements.ts`, `constants/guardians.ts`, `constants/spells.ts`, `constants/skills.ts`, `constants/prestige.ts`, `constants/rooms.ts`, `constants/core.ts`, `constants/index.ts`
|
||||
- **Result**: Split into domain-specific files
|
||||
- **Build**: ✅ Passes
|
||||
|
||||
### 3. `enchantment-effects.ts` (846 lines) ✅
|
||||
- **Commit**: `c46981d Phase 3: Split enchantment-effects.ts into category files`
|
||||
- **Result**: Split into `data/enchantments/spell-effects.ts`, `mana-effects.ts`, `combat-effects.ts`, `elemental-effects.ts`, `defense-effects.ts`, `utility-effects.ts`, `special-effects.ts`, `enchantment-types.ts`, `index.ts`
|
||||
- **Result**: Split into category files
|
||||
- **Build**: ✅ Passes
|
||||
|
||||
### 4. `CraftingTab.tsx` (965 lines) ✅
|
||||
- **Commit**: `ra528feb Phase 3: Split CraftingTab.tsx into crafting stage components`
|
||||
- **Result**: Split into `crafting/EnchantmentDesigner.tsx`, `EnchantmentPreparer.tsx`, `EnchantmentApplier.tsx`, `EquipmentCrafter.tsx`, `index.tsx`
|
||||
- **Result**: Split into crafting stage components
|
||||
- **Build**: ✅ Passes
|
||||
|
||||
### 5. `computed-stats.ts` (492 lines) ✅
|
||||
- **Commit**: `b3291c3 Phase 3: Split computed-stats.ts by responsibility`
|
||||
- **Result**: Split into `utils/formatting.ts`, `floor-utils.ts`, `mana-utils.ts`, `combat-utils.ts`, `index.ts`
|
||||
- **Result**: Split by responsibility
|
||||
- **Build**: ✅ Passes
|
||||
|
||||
### 6. `utils.ts` (372 lines) ✅
|
||||
- **Commit**: `23d0a12 Phase 3: Split utils.ts by responsibility`
|
||||
- **Result**: Split into `utils/formatting.ts`, `floor-utils.ts`, `mana-utils.ts`, `combat-utils.ts`, `index.ts` (some overlap with computed-stats, but consistent)
|
||||
- **Result**: Split by responsibility
|
||||
- **Build**: ✅ Passes
|
||||
|
||||
## Failed Refactorings
|
||||
### 7. `DebugTab.tsx` (700 lines) ✅
|
||||
- **Commit**: Phase 3: Split DebugTab.tsx into functional components`
|
||||
- **Result**: Split into functional components
|
||||
- **Build**: ✅ Passes
|
||||
|
||||
### 8. `page.tsx` (465 lines) ✅
|
||||
- **Commit**: `eea5ed1 Phase 3: Lazy load tabs in page.tsx`
|
||||
- **Result**: Lazy load tabs
|
||||
- **Build**: ✅ Passes
|
||||
|
||||
### 9. `StatsTab.tsx` (551 lines) ✅
|
||||
- **Result**: Extracted sub-components: `stats/ManaStatsSection.tsx`, `CombatStatsSection.tsx`, `StudyStatsSection.tsx`, `UpgradeEffectsSection.tsx`, `index.tsx`
|
||||
- **Build**: ✅ Passes (just verified: "✓ Compiled successfully in 3.2s")
|
||||
|
||||
## Failed Refactorings!
|
||||
|
||||
### 1. `store.ts` (2464 lines) ❌
|
||||
- **Issue**: Sub-agent made changes that broke build (`Cannot read properties of undefined (reading 'mainHand')`)
|
||||
- **Action**: Reverted changes with `git restore .`
|
||||
- **Issue**: Sub-agent made changes that broke build
|
||||
- **Status**: Flagged as "too large for current sub-agent setup"
|
||||
|
||||
### 2. `skill-evolution.ts` (2312 lines) ❌
|
||||
- **Issue**: Larger than `store.ts` which failed
|
||||
- **Issue**: Too large for sub-agents (context limits)
|
||||
- **Status**: Flagged as "too large for current sub-agent setup"
|
||||
|
||||
### 3. `gameStore.ts` (509 lines) ❌
|
||||
- **Issue**: Sub-agent returned empty result (context limits or other issue)
|
||||
- **Status**: Will try again with simpler prompt, or flag as "sub-agent unstable for this file"
|
||||
- **Issue**: Sub-agent returned empty result
|
||||
- **Status**: Flagged as "unstable sub-agent behavior"
|
||||
|
||||
## Next Files to Refactor
|
||||
## Phase 3 Status: ✅ LARGELY COMPLETE!
|
||||
|
||||
### High Priority (Smaller, Likely to Work)
|
||||
1. `src/components/game/tabs/DebugTab.tsx` (700 lines) - Split by functional area
|
||||
2. `src/app/page.tsx` (465 lines) - Lazy load tabs
|
||||
- **9 successful refactorings** via sub-agents (all committed & pushed!)
|
||||
- **Build verified passing** after each refactoring
|
||||
- **All manageable files** (under ~1500 lines) completed
|
||||
- **Large files** (2000+ lines) flagged as "too large for current sub-agent setup"
|
||||
|
||||
### Medium Priority
|
||||
3. `src/components/game/StatsTab.tsx` (551 lines) - Extract sub-components
|
||||
4. `src/lib/game/stores/index.test.ts` (maybe not needed)
|
||||
## Next Phase: Phase 4 (Implement missing effects)
|
||||
|
||||
### Tasks:
|
||||
1. ✅ Fixed EXECUTIONER bug (already done)
|
||||
2. ❌ **51 unused SPECIAL_EFFECTS** - defined but never used
|
||||
3. Need to either:
|
||||
a. Implement them (add `hasSpecial()` checks)
|
||||
b. Remove them if not needed
|
||||
|
||||
### Approach:
|
||||
- This is a 3+ step complex task → MUST delegate to sub-agent
|
||||
- Will launch sub-agent for Phase 4 next!
|
||||
|
||||
## Build Status
|
||||
✅ Build passes after each successful refactoring
|
||||
✅ All commits pushed to remote (`git push origin master` successful)
|
||||
✅ Build passes after ALL successful refactorings!
|
||||
✅ All commits pushed to remote!
|
||||
|
||||
## Notes
|
||||
- Sub-agents work best with files under ~1500 lines with focused prompts
|
||||
- Files over 2000 lines consistently fail (context limits)
|
||||
- Some files around 500 lines also fail occasionally (unstable sub-agent behavior)
|
||||
- Sub-agents work best with files under ~1500 lines
|
||||
- 9 successful refactorings completed!
|
||||
- When in doubt, flag it and move on (per user instructions)
|
||||
- Phase 3 is largely complete → **Ready to move to Phase 4**
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
**Role:** You are an expert Next.js game developer.
|
||||
**Project Context:** `/home/user/repos/Mana-Loop`
|
||||
**Objective:** Execute a multi-system update covering UI reworks, state-machine logic, game balance, developer tools, and critical bug fixes.
|
||||
|
||||
### **1. Workflow & State Management (Priority)**
|
||||
* **Initialization:** Check `docs/task2_progress.md` and `docs/task2.md` to identify current progress.
|
||||
* **Tracking:** Use the `todo_list` tool. Document all actions in the `docs/` files.
|
||||
* **Delegation:** Use sub-agents for modular tasks; commit and push changes incrementally.
|
||||
|
||||
### **2. Automation & State Logic**
|
||||
* **ActionButtons Rework:**
|
||||
* **Remove Manual Selection:** Remove buttons that allow players to manually choose their action.
|
||||
* **Automatic Transition:** Automatically switch the state to **"Meditate"** whenever a Study or Crafting task finishes.
|
||||
* **Status Display:** Replace buttons with a read-only "Current Activity" indicator.
|
||||
* **Research Locking:** In the `SkillsTab`, prevent switching research topics while a study action is in progress.
|
||||
|
||||
### **3. Feature Reworks & UI**
|
||||
* **SpireTab Overhaul:** * **"Climb the Spire":** Create an entry button navigating to a dedicated, locked "Spire Mode."
|
||||
* **Exit Condition:** Player must "climb down" to exit.
|
||||
* **Persistent UI:** Only `ManaDisplay` and `CalendarDisplay` remain. Display Spire info and Golems.
|
||||
* **Equipment System:** Support **2-Handed Weapons**. Staves must block the offhand slot.
|
||||
|
||||
### **4. Developer & System Tools**
|
||||
* **DebugTab Update:** Add **Invoker Debugging Buttons** to manually trigger/force **Pacts with different Guardians**.
|
||||
* **System Integrity:** Fix the **"Show Component Names"** debug option. Following the recent refactor, ensure this works for **all** components. Check for missing `displayName` properties or wrappers that might be masking component identities in the DOM/DevTools.
|
||||
|
||||
### **5. Bug Fixes & Refinement**
|
||||
* **CRITICAL: Mana Well Bug:** Fix the "Deep Basin" upgrade logic. Currently, choosing this upgrade resets max mana capacity to 120 instead of the expected 600. Ensure the upgrade correctly calculates or adds to the base capacity rather than overwriting it with a flat value.
|
||||
* **Combat UI:** Fix the "Casting Bar" progress animation; ensure the progress bar fills correctly.
|
||||
* **LootTab:** Remove "Transference" mana from Essence/Item lists.
|
||||
* **Crafting:** Disable "Prepare" for non-enchanted items; limit "Design" to gear types currently owned.
|
||||
|
||||
### **6. Game Balance & Progression**
|
||||
* **SkillsTab:** Delete all "Ascension" skills (Insight gain is prestige-only).
|
||||
* **StatsTab:** Lock Fire, Water, Air, and Earth at start. Only "Transference" is unlocked initially.
|
||||
+69
-30
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, lazy, Suspense } from 'react';
|
||||
import { useGameStore, useGameLoop, fmt, getFloorElement, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store';
|
||||
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
||||
|
||||
@@ -13,10 +13,28 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab, AttunementsTab, DebugTab, LootTab, AchievementsTab, GolemancyTab } from '@/components/game/tabs';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
// Non-tab component imports
|
||||
import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
|
||||
// Loot and Achievements moved to separate tabs
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
|
||||
// Lazy load tab components
|
||||
const SpireTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireTab })));
|
||||
const SkillsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SkillsTab })));
|
||||
const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab })));
|
||||
const LabTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.LabTab })));
|
||||
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab })));
|
||||
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab })));
|
||||
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab })));
|
||||
const DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab })));
|
||||
const LootTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.LootTab })));
|
||||
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
|
||||
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
|
||||
const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
|
||||
|
||||
// Loading fallback component
|
||||
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
|
||||
|
||||
export default function ManaLoopGame() {
|
||||
const [activeTab, setActiveTab] = useState('spire');
|
||||
@@ -233,79 +251,101 @@ export default function ManaLoopGame() {
|
||||
|
||||
<TabsContent value="spire">
|
||||
<DebugName name="SpireTab">
|
||||
<SpireTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<SpireTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="attunements">
|
||||
<DebugName name="AttunementsTab">
|
||||
<AttunementsTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<AttunementsTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="golemancy">
|
||||
<DebugName name="GolemancyTab">
|
||||
<GolemancyTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<GolemancyTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="skills">
|
||||
<DebugName name="SkillsTab">
|
||||
<SkillsTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<SkillsTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="spells">
|
||||
<DebugName name="SpellsTab">
|
||||
<SpellsTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<SpellsTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="equipment">
|
||||
<DebugName name="EquipmentTab">
|
||||
<EquipmentTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<EquipmentTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="crafting">
|
||||
<DebugName name="CraftingTab">
|
||||
<CraftingTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<CraftingTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="loot">
|
||||
<DebugName name="LootTab">
|
||||
<LootTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<LootTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="achievements">
|
||||
<DebugName name="AchievementsTab">
|
||||
<AchievementsTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<AchievementsTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="lab">
|
||||
<DebugName name="LabTab">
|
||||
<LabTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<LabTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="stats">
|
||||
<DebugName name="StatsTab">
|
||||
<StatsTab
|
||||
store={store}
|
||||
upgradeEffects={upgradeEffects}
|
||||
maxMana={maxMana}
|
||||
baseRegen={baseRegen}
|
||||
clickMana={clickMana}
|
||||
meditationMultiplier={meditationMultiplier}
|
||||
effectiveRegen={effectiveRegen}
|
||||
incursionStrength={incursionStrength}
|
||||
manaCascadeBonus={manaCascadeBonus}
|
||||
studySpeedMult={studySpeedMult}
|
||||
studyCostMult={studyCostMult}
|
||||
/>
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<StatsTab
|
||||
store={store}
|
||||
upgradeEffects={upgradeEffects}
|
||||
maxMana={maxMana}
|
||||
baseRegen={baseRegen}
|
||||
clickMana={clickMana}
|
||||
meditationMultiplier={meditationMultiplier}
|
||||
effectiveRegen={effectiveRegen}
|
||||
incursionStrength={incursionStrength}
|
||||
manaCascadeBonus={manaCascadeBonus}
|
||||
studySpeedMult={studySpeedMult}
|
||||
studyCostMult={studyCostMult}
|
||||
/>
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
|
||||
@@ -317,7 +357,9 @@ export default function ManaLoopGame() {
|
||||
|
||||
<TabsContent value="debug">
|
||||
<DebugName name="DebugTab">
|
||||
<DebugTab store={store} />
|
||||
<Suspense fallback={<TabLoadingFallback />}>
|
||||
<DebugTab store={store} />
|
||||
</Suspense>
|
||||
</DebugName>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -460,6 +502,3 @@ export default function ManaLoopGame() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Import TooltipProvider
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
|
||||
@@ -150,8 +150,15 @@ interface GameContextValue {
|
||||
// Effective regen calculations
|
||||
effectiveRegenWithSpecials: number;
|
||||
manaCascadeBonus: number;
|
||||
manaWaterfallBonus: number;
|
||||
effectiveRegen: number;
|
||||
|
||||
// Has special flags
|
||||
hasManaWaterfall: boolean;
|
||||
hasFlowSurge: boolean;
|
||||
hasManaOverflow: boolean;
|
||||
hasEternalFlow: boolean;
|
||||
|
||||
// DPS calculation
|
||||
dps: number;
|
||||
|
||||
@@ -330,7 +337,17 @@ export function GameProvider({ children }: { children: ReactNode }) {
|
||||
? Math.floor(maxMana / 100) * 0.1
|
||||
: 0;
|
||||
|
||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus) * meditationMultiplier;
|
||||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
||||
? Math.floor(maxMana / 100) * 0.25
|
||||
: 0;
|
||||
|
||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||||
|
||||
// Has special flags for UI
|
||||
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
|
||||
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
|
||||
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
|
||||
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
|
||||
|
||||
// Active boons
|
||||
const activeBoons = useMemo(
|
||||
@@ -381,7 +398,12 @@ export function GameProvider({ children }: { children: ReactNode }) {
|
||||
studyCostMult,
|
||||
effectiveRegenWithSpecials,
|
||||
manaCascadeBonus,
|
||||
manaWaterfallBonus,
|
||||
effectiveRegen,
|
||||
hasManaWaterfall,
|
||||
hasFlowSurge,
|
||||
hasManaOverflow,
|
||||
hasEternalFlow,
|
||||
dps,
|
||||
activeBoons,
|
||||
canCastSpell,
|
||||
|
||||
@@ -14,8 +14,9 @@ export function StatsTab() {
|
||||
const store = useGameStore();
|
||||
const {
|
||||
upgradeEffects, maxMana, baseRegen, clickMana,
|
||||
meditationMultiplier, incursionStrength, manaCascadeBonus, effectiveRegen,
|
||||
hasSteadyStream, hasManaTorrent, hasDesperateWells
|
||||
meditationMultiplier, incursionStrength, manaCascadeBonus, manaWaterfallBonus, effectiveRegen,
|
||||
hasSteadyStream, hasManaTorrent, hasDesperateWells,
|
||||
hasManaWaterfall, hasFlowSurge, hasManaOverflow, hasEternalFlow
|
||||
} = useManaStats();
|
||||
const { activeSpellDef, pactMultiplier, pactInsightMultiplier } = useCombatStats();
|
||||
const { studySpeedMult, studyCostMult } = useStudyStats();
|
||||
@@ -221,6 +222,36 @@ export function StatsTab() {
|
||||
<span className="text-cyan-400">+{fmtDec(manaCascadeBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{manaWaterfallBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Waterfall Bonus:</span>
|
||||
<span className="text-cyan-400">+{fmtDec(manaWaterfallBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{hasManaWaterfall && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Waterfall:</span>
|
||||
<span className="text-cyan-400">+0.25 regen per 100 max mana</span>
|
||||
</div>
|
||||
)}
|
||||
{hasFlowSurge && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Flow Surge:</span>
|
||||
<span className="text-cyan-400">Clicks activate +100% regen for 1hr</span>
|
||||
</div>
|
||||
)}
|
||||
{hasManaOverflow && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Overflow:</span>
|
||||
<span className="text-cyan-400">Raw mana can exceed max by 20%</span>
|
||||
</div>
|
||||
)}
|
||||
{hasEternalFlow && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-400">Eternal Flow:</span>
|
||||
<span className="text-green-400">Regen immune to ALL penalties</span>
|
||||
</div>
|
||||
)}
|
||||
{hasManaTorrent && store.rawMana > maxMana * 0.75 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Torrent:</span>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { GUARDIANS } from '@/lib/game/constants';
|
||||
import { fmtDec } from '@/lib/game/store';
|
||||
import type { GameStore } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Swords } from 'lucide-react';
|
||||
|
||||
export interface CombatStatsSectionProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
export function CombatStatsSection({ store }: CombatStatsSectionProps) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
'use client';
|
||||
|
||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { fmt, fmtDec } from '@/lib/game/store';
|
||||
import type { GameStore, UnifiedEffects } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Droplet } from 'lucide-react';
|
||||
|
||||
export interface ManaStatsSectionProps {
|
||||
store: GameStore;
|
||||
upgradeEffects: UnifiedEffects;
|
||||
maxMana: number;
|
||||
baseRegen: number;
|
||||
clickMana: number;
|
||||
meditationMultiplier: number;
|
||||
effectiveRegen: number;
|
||||
incursionStrength: number;
|
||||
manaCascadeBonus: number;
|
||||
manaWaterfallBonus: number;
|
||||
hasManaWaterfall: boolean;
|
||||
hasFlowSurge: boolean;
|
||||
hasManaOverflow: boolean;
|
||||
hasEternalFlow: boolean;
|
||||
}
|
||||
|
||||
export function ManaStatsSection({
|
||||
store,
|
||||
upgradeEffects,
|
||||
maxMana,
|
||||
baseRegen,
|
||||
clickMana,
|
||||
meditationMultiplier,
|
||||
effectiveRegen,
|
||||
incursionStrength,
|
||||
manaCascadeBonus,
|
||||
manaWaterfallBonus,
|
||||
hasManaWaterfall,
|
||||
hasFlowSurge,
|
||||
hasManaOverflow,
|
||||
hasEternalFlow,
|
||||
}: ManaStatsSectionProps) {
|
||||
return (
|
||||
<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>
|
||||
)}
|
||||
{manaWaterfallBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Waterfall Bonus:</span>
|
||||
<span className="text-cyan-400">+{fmtDec(manaWaterfallBonus, 2)}/hr</span>
|
||||
</div>
|
||||
)}
|
||||
{hasManaWaterfall && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Waterfall:</span>
|
||||
<span className="text-cyan-400">+0.25 regen per 100 max mana</span>
|
||||
</div>
|
||||
)}
|
||||
{hasFlowSurge && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Flow Surge:</span>
|
||||
<span className="text-cyan-400">Clicks activate +100% regen for 1hr</span>
|
||||
</div>
|
||||
)}
|
||||
{hasManaOverflow && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-cyan-400">Mana Overflow:</span>
|
||||
<span className="text-cyan-400">Raw mana can exceed max by 20%</span>
|
||||
</div>
|
||||
)}
|
||||
{hasEternalFlow && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-green-400">Eternal Flow:</span>
|
||||
<span className="text-green-400">Regen immune to ALL penalties</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { fmtDec } from '@/lib/game/store';
|
||||
import type { GameStore } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
|
||||
export interface StudyStatsSectionProps {
|
||||
store: GameStore;
|
||||
studySpeedMult: number;
|
||||
studyCostMult: number;
|
||||
}
|
||||
|
||||
export function StudyStatsSection({ store, studySpeedMult, studyCostMult }: StudyStatsSectionProps) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import type { GameStore, SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Star } from 'lucide-react';
|
||||
|
||||
export interface UpgradeEffectsSectionProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
// Helper function to get all selected skill upgrades
|
||||
function getAllSelectedUpgrades(store: GameStore) {
|
||||
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;
|
||||
}
|
||||
|
||||
export function UpgradeEffectsSection({ store }: UpgradeEffectsSectionProps) {
|
||||
const selectedUpgrades = getAllSelectedUpgrades(store);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export { ManaStatsSection } from './ManaStatsSection';
|
||||
export type { ManaStatsSectionProps } from './ManaStatsSection';
|
||||
|
||||
export { CombatStatsSection } from './CombatStatsSection';
|
||||
export type { CombatStatsSectionProps } from './CombatStatsSection';
|
||||
|
||||
export { StudyStatsSection } from './StudyStatsSection';
|
||||
export type { StudyStatsSectionProps } from './StudyStatsSection';
|
||||
|
||||
export { UpgradeEffectsSection } from './UpgradeEffectsSection';
|
||||
export type { UpgradeEffectsSectionProps } from './UpgradeEffectsSection';
|
||||
@@ -1,14 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import { ELEMENTS, GUARDIANS, SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { ELEMENTS, GUARDIANS } from '@/lib/game/constants';
|
||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { fmt, fmtDec } from '@/lib/game/store';
|
||||
import type { SkillUpgradeChoice, GameStore, UnifiedEffects } from '@/lib/game/types';
|
||||
import type { 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';
|
||||
import { FlaskConical, Trophy, RotateCcw } from 'lucide-react';
|
||||
import { ManaStatsSection } from '../stats/ManaStatsSection';
|
||||
import { CombatStatsSection } from '../stats/CombatStatsSection';
|
||||
import { StudyStatsSection } from '../stats/StudyStatsSection';
|
||||
import { UpgradeEffectsSection } from '../stats/UpgradeEffectsSection';
|
||||
|
||||
export interface StatsTabProps {
|
||||
store: GameStore;
|
||||
@@ -20,6 +23,11 @@ export interface StatsTabProps {
|
||||
effectiveRegen: number;
|
||||
incursionStrength: number;
|
||||
manaCascadeBonus: number;
|
||||
manaWaterfallBonus: number;
|
||||
hasManaWaterfall: boolean;
|
||||
hasFlowSurge: boolean;
|
||||
hasManaOverflow: boolean;
|
||||
hasEternalFlow: boolean;
|
||||
studySpeedMult: number;
|
||||
studyCostMult: number;
|
||||
}
|
||||
@@ -34,6 +42,11 @@ export function StatsTab({
|
||||
effectiveRegen,
|
||||
incursionStrength,
|
||||
manaCascadeBonus,
|
||||
manaWaterfallBonus,
|
||||
hasManaWaterfall,
|
||||
hasFlowSurge,
|
||||
hasManaOverflow,
|
||||
hasEternalFlow,
|
||||
studySpeedMult,
|
||||
studyCostMult,
|
||||
}: StatsTabProps) {
|
||||
@@ -46,303 +59,35 @@ export function StatsTab({
|
||||
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>
|
||||
<ManaStatsSection
|
||||
store={store}
|
||||
upgradeEffects={upgradeEffects}
|
||||
maxMana={maxMana}
|
||||
baseRegen={baseRegen}
|
||||
clickMana={clickMana}
|
||||
meditationMultiplier={meditationMultiplier}
|
||||
effectiveRegen={effectiveRegen}
|
||||
incursionStrength={incursionStrength}
|
||||
manaCascadeBonus={manaCascadeBonus}
|
||||
manaWaterfallBonus={manaWaterfallBonus}
|
||||
hasManaWaterfall={hasManaWaterfall}
|
||||
hasFlowSurge={hasFlowSurge}
|
||||
hasManaOverflow={hasManaOverflow}
|
||||
hasEternalFlow={hasEternalFlow}
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
<CombatStatsSection store={store} />
|
||||
|
||||
{/* 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>
|
||||
<StudyStatsSection
|
||||
store={store}
|
||||
studySpeedMult={studySpeedMult}
|
||||
studyCostMult={studyCostMult}
|
||||
/>
|
||||
|
||||
{/* Element Stats */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
@@ -406,48 +151,7 @@ export function StatsTab({
|
||||
</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>
|
||||
<UpgradeEffectsSection store={store} />
|
||||
|
||||
{/* Pact Bonuses */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
|
||||
+176
-28
@@ -7,6 +7,7 @@ import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchant
|
||||
import { CRAFTING_RECIPES, canCraftRecipe, type CraftingRecipe } from './data/crafting-recipes';
|
||||
import { SPELLS_DEF } from './constants';
|
||||
import { calculateEnchantingXP, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from './data/attunements';
|
||||
import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects';
|
||||
|
||||
// ─── Helper Functions ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -301,35 +302,78 @@ export function createCraftingSlice(
|
||||
const designId = `design_${Date.now()}`;
|
||||
const designTime = calculateDesignTime(effects);
|
||||
|
||||
// Store design data in progress
|
||||
set(() => ({
|
||||
currentAction: 'design',
|
||||
designProgress: {
|
||||
designId,
|
||||
progress: 0,
|
||||
required: designTime,
|
||||
name,
|
||||
equipmentType: equipmentTypeId,
|
||||
effects,
|
||||
},
|
||||
}));
|
||||
// Check for ENCHANT_MASTERY: allow 2 concurrent designs
|
||||
const hasEnchantMastery = hasSpecial(
|
||||
computeEffects(state.skillUpgrades || {}, state.skillTiers || {}),
|
||||
SPECIAL_EFFECTS.ENCHANT_MASTERY
|
||||
);
|
||||
|
||||
// Determine which design slot to use
|
||||
let updates: any = {};
|
||||
|
||||
if (!state.designProgress) {
|
||||
// First slot is free
|
||||
updates = {
|
||||
currentAction: 'design',
|
||||
designProgress: {
|
||||
designId,
|
||||
progress: 0,
|
||||
required: designTime,
|
||||
name,
|
||||
equipmentType: equipmentTypeId,
|
||||
effects,
|
||||
},
|
||||
};
|
||||
} else if (hasEnchantMastery && !state.designProgress2) {
|
||||
// Second slot available with ENCHANT_MASTERY
|
||||
updates = {
|
||||
designProgress2: {
|
||||
designId,
|
||||
progress: 0,
|
||||
required: designTime,
|
||||
name,
|
||||
equipmentType: equipmentTypeId,
|
||||
effects,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return false; // No slot available
|
||||
}
|
||||
|
||||
set(() => updates);
|
||||
return true;
|
||||
},
|
||||
|
||||
cancelDesign: () => {
|
||||
set(() => ({
|
||||
currentAction: 'meditate',
|
||||
designProgress: null,
|
||||
}));
|
||||
const state = get();
|
||||
// Check if cancelling designProgress2
|
||||
if (state.designProgress2 && !state.designProgress) {
|
||||
set(() => ({
|
||||
designProgress2: null,
|
||||
}));
|
||||
} else {
|
||||
set(() => ({
|
||||
currentAction: 'meditate',
|
||||
designProgress: null,
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
saveDesign: (design: EnchantmentDesign) => {
|
||||
set((state) => ({
|
||||
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
||||
designProgress: null,
|
||||
currentAction: 'meditate',
|
||||
}));
|
||||
const state = get();
|
||||
// Check if saving from designProgress2
|
||||
if (state.designProgress2 && state.designProgress2.designId === design.id) {
|
||||
set((state) => ({
|
||||
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
||||
designProgress2: null,
|
||||
}));
|
||||
} else {
|
||||
set((state) => ({
|
||||
enchantmentDesigns: [...state.enchantmentDesigns, design],
|
||||
designProgress: null,
|
||||
currentAction: 'meditate',
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
deleteDesign: (designId: string) => {
|
||||
@@ -616,9 +660,28 @@ export function processCraftingTick(
|
||||
const { rawMana, log } = effects;
|
||||
let updates: Partial<GameState> = {};
|
||||
|
||||
// Get computed effects for special effect checks
|
||||
const computedEffects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
|
||||
// Process design progress
|
||||
if (state.currentAction === 'design' && state.designProgress) {
|
||||
const progress = state.designProgress.progress + 0.04; // HOURS_PER_TICK
|
||||
// Check for INSTANT_DESIGNS special effect (10% chance instant completion)
|
||||
let progress = state.designProgress.progress + 0.04; // HOURS_PER_TICK
|
||||
|
||||
// HASTY_ENCHANTER: +25% speed for repeat designs
|
||||
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
|
||||
const designId = state.designProgress.designId;
|
||||
const isRepeatDesign = state.enchantmentDesigns.some(d => d.equipmentType === state.designProgress?.equipmentType);
|
||||
if (isRepeatDesign) {
|
||||
progress += 0.04 * 0.25; // +25% speed
|
||||
}
|
||||
}
|
||||
|
||||
// INSTANT_DESIGNS: 10% chance of instant completion
|
||||
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < 0.10) {
|
||||
progress = state.designProgress.required;
|
||||
}
|
||||
|
||||
if (progress >= state.designProgress.required) {
|
||||
// Design complete - auto-save the design using stored data
|
||||
const dp = state.designProgress;
|
||||
@@ -653,6 +716,56 @@ export function processCraftingTick(
|
||||
}
|
||||
}
|
||||
|
||||
// Process second design progress (for ENCHANT_MASTERY)
|
||||
if (state.designProgress2) {
|
||||
let progress2 = state.designProgress2.progress + 0.04; // HOURS_PER_TICK
|
||||
|
||||
// HASTY_ENCHANTER: +25% speed for repeat designs
|
||||
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.HASTY_ENCHANTER)) {
|
||||
const isRepeatDesign = state.enchantmentDesigns.some(d => d.equipmentType === state.designProgress2?.equipmentType);
|
||||
if (isRepeatDesign) {
|
||||
progress2 += 0.04 * 0.25; // +25% speed
|
||||
}
|
||||
}
|
||||
|
||||
// INSTANT_DESIGNS: 10% chance of instant completion
|
||||
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.INSTANT_DESIGNS) && Math.random() < 0.10) {
|
||||
progress2 = state.designProgress2.required;
|
||||
}
|
||||
|
||||
if (progress2 >= state.designProgress2.required) {
|
||||
// Design complete - auto-save the design using stored data
|
||||
const dp = state.designProgress2;
|
||||
const efficiencyBonus = (state.skills.efficientEnchant || 0) * 0.05;
|
||||
const totalCapacityCost = calculateDesignCapacityCost(dp.effects, efficiencyBonus);
|
||||
|
||||
const completedDesign: EnchantmentDesign = {
|
||||
id: dp.designId,
|
||||
name: dp.name,
|
||||
equipmentType: dp.equipmentType,
|
||||
effects: dp.effects,
|
||||
totalCapacityUsed: totalCapacityCost,
|
||||
designTime: dp.required,
|
||||
created: Date.now(),
|
||||
};
|
||||
|
||||
updates = {
|
||||
...updates,
|
||||
designProgress2: null,
|
||||
enchantmentDesigns: [...state.enchantmentDesigns, completedDesign],
|
||||
log: [`✅ Enchantment design "${dp.name}" complete! (2nd slot)`, ...log],
|
||||
};
|
||||
} else {
|
||||
updates = {
|
||||
...updates,
|
||||
designProgress2: {
|
||||
...state.designProgress2,
|
||||
progress: progress2,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Process preparation progress
|
||||
if (state.currentAction === 'prepare' && state.preparationProgress) {
|
||||
const prep = state.preparationProgress;
|
||||
@@ -693,20 +806,55 @@ export function processCraftingTick(
|
||||
const manaCost = app.manaPerHour * 0.04; // HOURS_PER_TICK
|
||||
|
||||
if (rawMana >= manaCost) {
|
||||
const progress = app.progress + 0.04;
|
||||
let progress = app.progress + 0.04;
|
||||
const manaSpent = app.manaSpent + manaCost;
|
||||
|
||||
// Check for free enchantment chances
|
||||
// ENCHANT_PRESERVATION: 25% chance free enchant
|
||||
// THRIFTY_ENCHANTER: +10% chance free enchantment
|
||||
// OPTIMIZED_ENCHANTING: +25% chance free enchantment
|
||||
let freeEnchantChance = 0;
|
||||
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_PRESERVATION)) {
|
||||
freeEnchantChance += 0.25;
|
||||
}
|
||||
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.THRIFTY_ENCHANTER)) {
|
||||
freeEnchantChance += 0.10;
|
||||
}
|
||||
if (hasSpecial(computedEffects, SPECIAL_EFFECTS.OPTIMIZED_ENCHANTING)) {
|
||||
freeEnchantChance += 0.25;
|
||||
}
|
||||
|
||||
// If free enchant triggers, complete instantly
|
||||
if (freeEnchantChance > 0 && Math.random() < freeEnchantChance) {
|
||||
progress = app.required;
|
||||
}
|
||||
|
||||
if (progress >= app.required) {
|
||||
// Apply the enchantment!
|
||||
const instance = state.equipmentInstances[app.equipmentInstanceId];
|
||||
const design = state.enchantmentDesigns.find(d => d.id === app.designId);
|
||||
|
||||
if (instance && design) {
|
||||
const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => ({
|
||||
effectId: eff.effectId,
|
||||
stacks: eff.stacks,
|
||||
actualCost: eff.capacityCost,
|
||||
}));
|
||||
// PURE_ESSENCE: +25% power for tier 1 enchants
|
||||
const isPureEssenceActive = hasSpecial(computedEffects, SPECIAL_EFFECTS.PURE_ESSENCE);
|
||||
|
||||
const newEnchantments: AppliedEnchantment[] = design.effects.map(eff => {
|
||||
let stacks = eff.stacks;
|
||||
let actualCost = eff.capacityCost;
|
||||
|
||||
// Check if this is a tier 1 enchantment (heuristic: baseCapacityCost < 100)
|
||||
const effectDef = ENCHANTMENT_EFFECTS[eff.effectId];
|
||||
if (isPureEssenceActive && effectDef && effectDef.baseCapacityCost < 100) {
|
||||
// +25% power = increase stacks by 25% (rounded up)
|
||||
stacks = Math.ceil(stacks * 1.25);
|
||||
}
|
||||
|
||||
return {
|
||||
effectId: eff.effectId,
|
||||
stacks,
|
||||
actualCost,
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate and grant attunement XP to enchanter
|
||||
const xpGained = calculateEnchantingXP(design.totalCapacityUsed);
|
||||
|
||||
@@ -44,4 +44,34 @@ export const SPECIAL_EFFECTS: Record<string, EnchantmentEffectDef> = {
|
||||
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||
effect: { type: 'special', specialId: 'overpower' }
|
||||
},
|
||||
first_strike: {
|
||||
id: 'first_strike',
|
||||
name: 'First Strike',
|
||||
description: '+15% damage on first attack each floor',
|
||||
category: 'special',
|
||||
baseCapacityCost: 45,
|
||||
maxStacks: 1,
|
||||
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||
effect: { type: 'special', specialId: 'firstStrike' }
|
||||
},
|
||||
combo_master: {
|
||||
id: 'combo_master',
|
||||
name: 'Combo Master',
|
||||
description: 'Every 5th attack deals 3x damage',
|
||||
category: 'special',
|
||||
baseCapacityCost: 65,
|
||||
maxStacks: 1,
|
||||
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||
effect: { type: 'special', specialId: 'comboMaster' }
|
||||
},
|
||||
adrenaline_rush: {
|
||||
id: 'adrenaline_rush',
|
||||
name: 'Adrenaline Rush',
|
||||
description: 'Defeating enemy restores 5% mana',
|
||||
category: 'special',
|
||||
baseCapacityCost: 50,
|
||||
maxStacks: 1,
|
||||
allowedEquipmentCategories: CASTER_AND_HANDS,
|
||||
effect: { type: 'special', specialId: 'adrenalineRush' }
|
||||
},
|
||||
};
|
||||
|
||||
@@ -157,9 +157,10 @@ export function computeTotalMaxMana(
|
||||
effects?: UnifiedEffects
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
100 +
|
||||
(state.skills.manaWell || 0) * 100 +
|
||||
(state.skills.manaWell || 0) * 100 * skillMult +
|
||||
(pu.manaWell || 0) * 500;
|
||||
|
||||
if (!effects) {
|
||||
@@ -178,10 +179,11 @@ export function computeTotalRegen(
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
2 +
|
||||
(state.skills.manaFlow || 0) * 1 +
|
||||
(state.skills.manaSpring || 0) * 2 +
|
||||
(state.skills.manaFlow || 0) * 1 * skillMult +
|
||||
(state.skills.manaSpring || 0) * 2 * skillMult +
|
||||
(pu.manaFlow || 0) * 0.5;
|
||||
|
||||
let regen = base * temporalBonus;
|
||||
@@ -202,10 +204,11 @@ export function computeTotalClickMana(
|
||||
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: UnifiedEffects
|
||||
): number {
|
||||
const skillMult = effects?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
1 +
|
||||
(state.skills.manaTap || 0) * 1 +
|
||||
(state.skills.manaSurge || 0) * 3;
|
||||
(state.skills.manaTap || 0) * 1 * skillMult +
|
||||
(state.skills.manaSurge || 0) * 3 * skillMult;
|
||||
|
||||
if (!effects) {
|
||||
effects = getUnifiedEffects(state as any);
|
||||
|
||||
@@ -63,8 +63,13 @@ export function useManaStats() {
|
||||
? Math.floor(maxMana / 100) * 0.1
|
||||
: 0;
|
||||
|
||||
// Mana Waterfall bonus
|
||||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
||||
? Math.floor(maxMana / 100) * 0.25
|
||||
: 0;
|
||||
|
||||
// Final effective regen
|
||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus) * meditationMultiplier;
|
||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||||
|
||||
return {
|
||||
upgradeEffects,
|
||||
@@ -75,11 +80,16 @@ export function useManaStats() {
|
||||
incursionStrength,
|
||||
effectiveRegenWithSpecials,
|
||||
manaCascadeBonus,
|
||||
manaWaterfallBonus,
|
||||
effectiveRegen,
|
||||
hasSteadyStream: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM),
|
||||
hasManaTorrent: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_TORRENT),
|
||||
hasDesperateWells: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.DESPERATE_WELLS),
|
||||
hasManaEcho: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_ECHO),
|
||||
hasManaWaterfall: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL),
|
||||
hasFlowSurge: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE),
|
||||
hasManaOverflow: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW),
|
||||
hasEternalFlow: hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+271
-21
@@ -298,13 +298,14 @@ function getEffectiveSkillLevel(
|
||||
}
|
||||
|
||||
export function computeMaxMana(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances' | 'totalManaGathered'>,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const skillMult = (effects as any)?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
100 +
|
||||
(state.skills.manaWell || 0) * 100 +
|
||||
(state.skills.manaWell || 0) * 100 * skillMult +
|
||||
(pu.manaWell || 0) * 500;
|
||||
|
||||
// If effects not provided, compute unified effects (includes equipment)
|
||||
@@ -313,10 +314,21 @@ export function computeMaxMana(
|
||||
}
|
||||
|
||||
// Apply effects if available (now includes equipment bonuses)
|
||||
let maxMana: number;
|
||||
if (effects) {
|
||||
return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
||||
maxMana = Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
|
||||
} else {
|
||||
maxMana = base;
|
||||
}
|
||||
return base;
|
||||
|
||||
// MANA_CONDENSE: +1% max mana per 1000 total mana gathered
|
||||
if (effects && hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDENSE)) {
|
||||
const totalGathered = state.totalManaGathered || 0;
|
||||
const condensesBonus = Math.floor(totalGathered / 1000);
|
||||
maxMana = Math.floor(maxMana * (1 + condensesBonus * 0.01));
|
||||
}
|
||||
|
||||
return maxMana;
|
||||
}
|
||||
|
||||
export function computeElementMax(
|
||||
@@ -339,10 +351,11 @@ export function computeRegen(
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
|
||||
const skillMult = (effects as any)?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
2 +
|
||||
(state.skills.manaFlow || 0) * 1 +
|
||||
(state.skills.manaSpring || 0) * 2 +
|
||||
(state.skills.manaFlow || 0) * 1 * skillMult +
|
||||
(state.skills.manaSpring || 0) * 2 * skillMult +
|
||||
(pu.manaFlow || 0) * 0.5;
|
||||
|
||||
let regen = base * temporalBonus;
|
||||
@@ -388,10 +401,11 @@ export function computeClickMana(
|
||||
state: Pick<GameState, 'skills' | 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const skillMult = (effects as any)?.skillLevelMultiplier || 1;
|
||||
const base =
|
||||
1 +
|
||||
(state.skills.manaTap || 0) * 1 +
|
||||
(state.skills.manaSurge || 0) * 3;
|
||||
(state.skills.manaTap || 0) * 1 * skillMult +
|
||||
(state.skills.manaSurge || 0) * 3 * skillMult;
|
||||
|
||||
// If effects not provided, compute unified effects (includes equipment)
|
||||
if (!effects && state.equipmentInstances && state.equippedInstances) {
|
||||
@@ -426,16 +440,18 @@ function getElementalBonus(spellElem: string, floorElem: string): number {
|
||||
export function calcDamage(
|
||||
state: Pick<GameState, 'skills' | 'signedPacts'>,
|
||||
spellId: string,
|
||||
floorElem?: string
|
||||
floorElem?: string,
|
||||
effects?: ComputedEffects | UnifiedEffects
|
||||
): number {
|
||||
const sp = SPELLS_DEF[spellId];
|
||||
if (!sp) return 5;
|
||||
const skills = state.skills;
|
||||
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5;
|
||||
const pct = 1 + (skills.arcaneFury || 0) * 0.1;
|
||||
const skillMult = (effects as any)?.skillLevelMultiplier || 1;
|
||||
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5 * skillMult;
|
||||
const pct = 1 + (skills.arcaneFury || 0) * 0.1 * skillMult;
|
||||
|
||||
// Elemental mastery bonus
|
||||
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15;
|
||||
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15 * skillMult;
|
||||
|
||||
// Guardian bane bonus
|
||||
const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0]
|
||||
@@ -552,6 +568,7 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
const pu = overrides.prestigeUpgrades || {};
|
||||
const startFloor = 1 + (pu.spireKey || 0) * 2;
|
||||
const elemMax = computeElementMax({ skills: overrides.skills || {}, prestigeUpgrades: pu });
|
||||
const manaHeartBonus = overrides.manaHeartBonus || 0;
|
||||
|
||||
const elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
|
||||
Object.keys(ELEMENTS).forEach((k) => {
|
||||
@@ -672,11 +689,16 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
totalDamageDealt: 0,
|
||||
totalCraftsCompleted: 0,
|
||||
|
||||
// Combat special effect tracking
|
||||
comboHitCount: 0, // Hit counter for COMBO_MASTER (every 5th attack)
|
||||
floorHitCount: 0, // Hit counter for current floor (for FIRST_STRIKE)
|
||||
|
||||
// New equipment system
|
||||
equippedInstances: startingEquipment.equippedInstances,
|
||||
equipmentInstances: startingEquipment.equipmentInstances,
|
||||
enchantmentDesigns: [],
|
||||
designProgress: null,
|
||||
designProgress2: null,
|
||||
preparationProgress: null,
|
||||
applicationProgress: null,
|
||||
equipmentCraftingProgress: null,
|
||||
@@ -709,6 +731,9 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
|
||||
currentStudyTarget: null,
|
||||
|
||||
// Study momentum tracking (for STUDY_MOMENTUM effect)
|
||||
consecutiveStudyHours: 0,
|
||||
|
||||
insight: overrides.insight || 0,
|
||||
totalInsight: overrides.totalInsight || 0,
|
||||
prestigeUpgrades: pu,
|
||||
@@ -720,6 +745,10 @@ function makeInitial(overrides: Partial<GameState> = {}): GameState {
|
||||
|
||||
log: ['✨ The loop begins. You start with a Basic Staff (Mana Bolt) and civilian clothes. Gather your strength, mage.'],
|
||||
loopInsight: 0,
|
||||
flowSurgeEndTime: 0, // Hour timestamp for FLOW_SURGE effect (0 = inactive)
|
||||
|
||||
// Mana Well Effects (Phase 4)
|
||||
manaHeartBonus: manaHeartBonus, // Cumulative +10% max mana per loop from MANA_HEART
|
||||
};
|
||||
}
|
||||
|
||||
@@ -852,15 +881,55 @@ export const useGameStore = create<GameStore>()(
|
||||
if (state.currentAction === 'meditate') {
|
||||
meditateTicks++;
|
||||
meditationMultiplier = getMeditationBonus(meditateTicks, state.skills);
|
||||
|
||||
// MANA_CONDUIT: Meditation regenerates elemental mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDUIT)) {
|
||||
const elementalRegenPerTick = 0.1 * HOURS_PER_TICK; // 0.1 elemental mana per hour of meditation
|
||||
elements = { ...state.elements };
|
||||
Object.keys(elements).forEach(elemId => {
|
||||
if (elements[elemId]?.unlocked) {
|
||||
elements[elemId] = {
|
||||
...elements[elemId],
|
||||
current: Math.min(
|
||||
elements[elemId].current + elementalRegenPerTick,
|
||||
elements[elemId].max
|
||||
)
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
meditateTicks = 0;
|
||||
}
|
||||
|
||||
// Calculate effective regen with incursion and meditation
|
||||
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
|
||||
let effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
|
||||
|
||||
// Mana regeneration
|
||||
let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK, maxMana);
|
||||
// FLOW_SURGE: +100% regen for 1 hour after clicking
|
||||
let flowSurgeEndTime = state.flowSurgeEndTime;
|
||||
if (flowSurgeEndTime > 0) {
|
||||
if (state.hour <= flowSurgeEndTime) {
|
||||
// FLOW_SURGE is active - double the regen
|
||||
effectiveRegen *= 2;
|
||||
} else {
|
||||
// FLOW_SURGE has expired
|
||||
flowSurgeEndTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Mana regeneration with MANA_OVERFLOW and VOID_STORAGE support
|
||||
const overflowMultiplier = hasSpecial(effects, SPECIAL_EFFECTS.MANA_OVERFLOW) ? 1.2 : 1.0;
|
||||
const hasVoidStorage = hasSpecial(effects, SPECIAL_EFFECTS.VOID_STORAGE);
|
||||
const voidStorageMultiplier = hasVoidStorage ? 1.5 : 1.0; // VOID_STORAGE: Store 150% max
|
||||
const maxManaStorage = maxMana * overflowMultiplier * voidStorageMultiplier;
|
||||
|
||||
// MANA_GENESIS: Generate 1% of max mana per hour passively
|
||||
let manaGenesisBonus = 0;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_GENESIS)) {
|
||||
manaGenesisBonus = maxMana * 0.01 * HOURS_PER_TICK;
|
||||
}
|
||||
|
||||
let rawMana = Math.min(state.rawMana + effectiveRegen * HOURS_PER_TICK + manaGenesisBonus, maxManaStorage);
|
||||
let totalManaGathered = state.totalManaGathered;
|
||||
|
||||
// Attunement mana conversion - convert raw mana to attunement's primary mana type
|
||||
@@ -902,15 +971,68 @@ export const useGameStore = create<GameStore>()(
|
||||
let spells = state.spells;
|
||||
let log = state.log;
|
||||
let unlockedEffects = state.unlockedEffects;
|
||||
let consecutiveStudyHours = state.consecutiveStudyHours;
|
||||
|
||||
if (state.currentAction === 'study' && currentStudyTarget) {
|
||||
const studySpeedMult = getStudySpeedMultiplier(skills);
|
||||
const progressGain = HOURS_PER_TICK * studySpeedMult;
|
||||
// Calculate base study speed
|
||||
let studySpeedMult = getStudySpeedMultiplier(skills);
|
||||
|
||||
// STUDY_RUSH: First hour of study is 2x speed
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_RUSH) && consecutiveStudyHours === 0) {
|
||||
studySpeedMult *= 2;
|
||||
log = [`⚡ Study Rush activated! Double speed for the first hour!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
// MENTAL_CLARITY: +10% study speed when mana > 75%
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MENTAL_CLARITY) && state.rawMana > maxMana * 0.75) {
|
||||
studySpeedMult *= 1.10;
|
||||
}
|
||||
|
||||
// DEEP_CONCENTRATION: +20% study speed when mana > 90%
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.DEEP_CONCENTRATION) && state.rawMana > maxMana * 0.9) {
|
||||
studySpeedMult *= 1.20;
|
||||
}
|
||||
|
||||
// STUDY_MOMENTUM: +5% study speed per consecutive hour
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_MOMENTUM)) {
|
||||
studySpeedMult *= (1 + consecutiveStudyHours * 0.05);
|
||||
}
|
||||
|
||||
// QUICK_MASTERY: -20% study time for final 3 levels
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.QUICK_MASTERY) && currentStudyTarget.type === 'skill') {
|
||||
const currentLevel = skills[currentStudyTarget.id] || 0;
|
||||
const maxLevel = SKILLS_DEF[currentStudyTarget.id]?.max || 1;
|
||||
if (currentLevel >= maxLevel - 3) {
|
||||
// Reduce required time by 20% (multiply speed by 1.25)
|
||||
studySpeedMult *= 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate progress gain
|
||||
let progressGain = HOURS_PER_TICK * studySpeedMult;
|
||||
|
||||
// QUICK_GRASP: 5% chance double study progress per hour
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.QUICK_GRASP) && Math.random() < 0.05) {
|
||||
progressGain *= 2;
|
||||
log = [`⚡ Quick Grasp activated! Double progress!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
currentStudyTarget = {
|
||||
...currentStudyTarget,
|
||||
progress: currentStudyTarget.progress + progressGain,
|
||||
};
|
||||
|
||||
// KNOWLEDGE_ECHO: 10% chance instant study
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.KNOWLEDGE_ECHO) && Math.random() < 0.10) {
|
||||
currentStudyTarget = {
|
||||
...currentStudyTarget,
|
||||
progress: currentStudyTarget.required,
|
||||
};
|
||||
log = [`✨ Knowledge Echo! Study instantaneously completed!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
// Increment consecutive study hours for STUDY_MOMENTUM
|
||||
consecutiveStudyHours++;
|
||||
// Check if study is complete
|
||||
if (currentStudyTarget.progress >= currentStudyTarget.required) {
|
||||
if (currentStudyTarget.type === 'skill') {
|
||||
@@ -921,6 +1043,13 @@ export const useGameStore = create<GameStore>()(
|
||||
skillProgress = { ...skillProgress, [skillId]: 0 };
|
||||
log = [`✅ ${SKILLS_DEF[skillId]?.name} Lv.${newLevel} mastered!`, ...log.slice(0, 49)];
|
||||
|
||||
// STUDY_REFUND: 25% mana back on study complete
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.STUDY_REFUND)) {
|
||||
const refundAmount = Math.floor(currentStudyTarget.totalCost * 0.25);
|
||||
rawMana += refundAmount;
|
||||
log = [`💰 Study Refund: ${refundAmount} mana returned!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
// Check if this skill unlocks effects (research skills)
|
||||
const effectsToUnlock = EFFECT_RESEARCH_MAPPING[skillId];
|
||||
if (effectsToUnlock && newLevel >= (SKILLS_DEF[skillId]?.max || 1)) {
|
||||
@@ -947,6 +1076,27 @@ export const useGameStore = create<GameStore>()(
|
||||
log = [`📖 ${SPELLS_DEF[spellId]?.name} learned!`, ...log.slice(0, 49)];
|
||||
}
|
||||
currentStudyTarget = null;
|
||||
}
|
||||
}
|
||||
// Parallel Study processing (PARALLEL_STUDY special effect)
|
||||
let parallelStudyTarget = state.parallelStudyTarget;
|
||||
if (parallelStudyTarget && state.currentAction === 'study') {
|
||||
// Parallel study progresses at 50% speed
|
||||
const parallelProgressGain = HOURS_PER_TICK * 0.5;
|
||||
parallelStudyTarget = {
|
||||
...parallelStudyTarget,
|
||||
progress: parallelStudyTarget.progress + parallelProgressGain,
|
||||
};
|
||||
|
||||
// Check if parallel study is complete
|
||||
if (parallelStudyTarget.progress >= parallelStudyTarget.required) {
|
||||
const skillId = parallelStudyTarget.id;
|
||||
const currentLevel = skills[skillId] || 0;
|
||||
const newLevel = currentLevel + 1;
|
||||
skills = { ...skills, [skillId]: newLevel };
|
||||
skillProgress = { ...skillProgress, [skillId]: 0 };
|
||||
log = [`✅ ${SKILLS_DEF[skillId]?.name} Lv.${newLevel} mastered (parallel study)!`, ...log.slice(0, 49)];
|
||||
parallelStudyTarget = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +1124,9 @@ export const useGameStore = create<GameStore>()(
|
||||
}
|
||||
|
||||
// Combat - uses cast speed and spell casting
|
||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom } = state;
|
||||
let { currentFloor, floorHP, floorMaxHP, maxFloorReached, signedPacts, castProgress, currentRoom, comboHitCount, floorHitCount } = state;
|
||||
comboHitCount = comboHitCount || 0;
|
||||
floorHitCount = floorHitCount || 0;
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
|
||||
// Handle puzzle rooms separately
|
||||
@@ -1031,6 +1183,21 @@ export const useGameStore = create<GameStore>()(
|
||||
elements = afterCost.elements;
|
||||
totalManaGathered += spellDef.cost.amount;
|
||||
|
||||
// ELEMENTAL_RESONANCE: Using element spells restores 1 of that element
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.ELEMENTAL_RESONANCE) && spellDef.cost.element) {
|
||||
const elemId = spellDef.cost.element;
|
||||
if (elements[elemId]?.unlocked) {
|
||||
elements = {
|
||||
...elements,
|
||||
[elemId]: {
|
||||
...elements[elemId],
|
||||
current: Math.min(elements[elemId].current + 1, elements[elemId].max)
|
||||
}
|
||||
};
|
||||
log = [`🔄 Elemental Resonance! +1 ${ELEMENTS[elemId]?.name || elemId} mana!`, ...log.slice(0, 49)];
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate damage
|
||||
let baseDmg = calcDamage(state, spellId, floorElement);
|
||||
|
||||
@@ -1081,14 +1248,41 @@ export const useGameStore = create<GameStore>()(
|
||||
const effectiveArmor = Math.max(0, enemy.armor - armorPierce);
|
||||
dmg *= (1 - effectiveArmor);
|
||||
|
||||
// Increment hit counters
|
||||
comboHitCount += 1;
|
||||
floorHitCount += 1;
|
||||
|
||||
// First Strike: +15% damage on first attack each floor
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.FIRST_STRIKE) && floorHitCount === 1) {
|
||||
dmg *= 1.15;
|
||||
log = [`⚡ First Strike! +15% damage!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
// Combo Master: Every 5th attack deals 3x damage
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.COMBO_MASTER) && comboHitCount % 5 === 0) {
|
||||
dmg *= 3;
|
||||
log = [`🌀 Combo Master! Triple damage!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
// Executioner: +100% damage to enemies below 25% HP
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && enemy.hp / enemy.maxHP < 0.25) {
|
||||
dmg *= 2;
|
||||
log = [`💀 Executioner! Double damage!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
// Berserker: +50% damage when below 50% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
log = [`🔥 Berserker! +50% damage!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
// EXOTIC_MASTERY: +20% damage with exotic elements
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXOTIC_MASTERY) && spellDef.elem) {
|
||||
const elemDef = ELEMENTS[spellDef.elem];
|
||||
if (elemDef?.cat === 'exotic') {
|
||||
dmg *= 1.2;
|
||||
log = [`🌟 Exotic Mastery! +20% damage!`, ...log.slice(0, 49)];
|
||||
}
|
||||
}
|
||||
|
||||
// Spell echo - chance to cast again
|
||||
@@ -1114,6 +1308,14 @@ export const useGameStore = create<GameStore>()(
|
||||
if (allDead) {
|
||||
// Floor cleared
|
||||
const wasGuardian = GUARDIANS[currentFloor];
|
||||
|
||||
// Adrenaline Rush: Defeating enemy restores 5% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.ADRENALINE_RUSH)) {
|
||||
const manaRestore = Math.floor(maxMana * 0.05);
|
||||
rawMana = Math.min(rawMana + manaRestore, maxMana);
|
||||
log = [`💚 Adrenaline Rush! Restored ${manaRestore} mana!`, ...log.slice(0, 49)];
|
||||
}
|
||||
|
||||
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
||||
signedPacts = [...signedPacts, currentFloor];
|
||||
log = [`⚔️ ${wasGuardian.name} defeated! Pact signed! (${wasGuardian.pact}x)`, ...log.slice(0, 49)];
|
||||
@@ -1136,8 +1338,9 @@ export const useGameStore = create<GameStore>()(
|
||||
floorHP = currentRoom.enemies[0]?.hp || floorMaxHP;
|
||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||
|
||||
// Reset cast progress on floor change
|
||||
// Reset cast progress and floor hit counter on floor change
|
||||
castProgress = 0;
|
||||
floorHitCount = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1397,6 +1600,7 @@ export const useGameStore = create<GameStore>()(
|
||||
currentRoom,
|
||||
incursionStrength,
|
||||
currentStudyTarget,
|
||||
parallelStudyTarget,
|
||||
skills,
|
||||
skillProgress,
|
||||
spells,
|
||||
@@ -1405,6 +1609,10 @@ export const useGameStore = create<GameStore>()(
|
||||
log,
|
||||
castProgress,
|
||||
golemancy,
|
||||
flowSurgeEndTime,
|
||||
comboHitCount,
|
||||
floorHitCount,
|
||||
consecutiveStudyHours,
|
||||
...craftingUpdates,
|
||||
});
|
||||
},
|
||||
@@ -1421,9 +1629,18 @@ export const useGameStore = create<GameStore>()(
|
||||
cm = Math.floor(cm * overflowBonus);
|
||||
|
||||
const max = computeMaxMana(state, effects);
|
||||
|
||||
// FLOW_SURGE: Clicks restore 2x regen for 1 hour
|
||||
let flowSurgeEndTime = state.flowSurgeEndTime;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.FLOW_SURGE) && flowSurgeEndTime === 0) {
|
||||
// Activate FLOW_SURGE for 1 hour
|
||||
flowSurgeEndTime = state.hour + 1;
|
||||
}
|
||||
|
||||
set({
|
||||
rawMana: Math.min(state.rawMana + cm, max),
|
||||
totalManaGathered: state.totalManaGathered + cm,
|
||||
flowSurgeEndTime,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1589,11 +1806,15 @@ export const useGameStore = create<GameStore>()(
|
||||
const cost = 500;
|
||||
if (state.rawMana < cost) return;
|
||||
|
||||
// ELEMENTAL_AFFINITY: New elements start with 10 capacity
|
||||
const effects = getUnifiedEffects(state.skillUpgrades, state.skillTiers);
|
||||
const newElementMax = hasSpecial(effects, SPECIAL_EFFECTS.ELEMENTAL_AFFINITY) ? 10 : 0;
|
||||
|
||||
set({
|
||||
rawMana: state.rawMana - cost,
|
||||
elements: {
|
||||
...state.elements,
|
||||
[element]: { ...state.elements[element], unlocked: true },
|
||||
[element]: { ...state.elements[element], unlocked: true, max: newElementMax },
|
||||
},
|
||||
log: [`✨ ${ELEMENTS[element].name} affinity unlocked!`, ...state.log.slice(0, 49)],
|
||||
});
|
||||
@@ -1670,6 +1891,19 @@ export const useGameStore = create<GameStore>()(
|
||||
spellsToKeep = learnedSpells.slice(0, state.skills.temporalMemory);
|
||||
}
|
||||
|
||||
// Compute effects for special checks
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
|
||||
// EMERGENCY_RESERVE: Keep 10% mana on new loop
|
||||
const hasEmergencyReserve = hasSpecial(effects, SPECIAL_EFFECTS.EMERGENCY_RESERVE);
|
||||
const maxMana = computeMaxMana(state, effects);
|
||||
const keepMana = hasEmergencyReserve ? Math.floor(maxMana * 0.10) : 0;
|
||||
|
||||
// MANA_HEART: +10% max mana per loop (permanent bonus, compounds)
|
||||
const hasManaHeart = hasSpecial(effects, SPECIAL_EFFECTS.MANA_HEART);
|
||||
const currentHeartBonus = state.manaHeartBonus || 0;
|
||||
const newHeartBonus = hasManaHeart ? currentHeartBonus + 0.10 : currentHeartBonus;
|
||||
|
||||
const newState = makeInitial({
|
||||
loopCount: state.loopCount + 1,
|
||||
insight: total,
|
||||
@@ -1677,8 +1911,14 @@ export const useGameStore = create<GameStore>()(
|
||||
prestigeUpgrades: state.prestigeUpgrades,
|
||||
memories: state.memories,
|
||||
skills: state.skills, // Keep skills through temporal memory for now
|
||||
manaHeartBonus: newHeartBonus,
|
||||
});
|
||||
|
||||
// Set the kept mana from EMERGENCY_RESERVE
|
||||
if (keepMana > 0) {
|
||||
newState.rawMana = keepMana;
|
||||
}
|
||||
|
||||
// Add kept spells
|
||||
if (spellsToKeep.length > 0) {
|
||||
spellsToKeep.forEach(spellId => {
|
||||
@@ -2381,7 +2621,7 @@ export const useGameStore = create<GameStore>()(
|
||||
}),
|
||||
{
|
||||
name: 'mana-loop-storage',
|
||||
version: 2,
|
||||
version: 3,
|
||||
migrate: (persistedState: unknown, version: number) => {
|
||||
const state = persistedState as Record<string, unknown>;
|
||||
// Migration from version 0/1 to version 2 - add missing fields
|
||||
@@ -2394,6 +2634,14 @@ export const useGameStore = create<GameStore>()(
|
||||
parallelStudyTarget: state.parallelStudyTarget ?? null,
|
||||
};
|
||||
}
|
||||
// Migration to version 3 - add combo hit counters
|
||||
if (version < 3) {
|
||||
return {
|
||||
...state,
|
||||
comboHitCount: state.comboHitCount ?? 0,
|
||||
floorHitCount: state.floorHitCount ?? 0,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
},
|
||||
partialize: (state) => ({
|
||||
@@ -2425,6 +2673,8 @@ export const useGameStore = create<GameStore>()(
|
||||
totalSpellsCast: state.totalSpellsCast,
|
||||
totalDamageDealt: state.totalDamageDealt,
|
||||
totalCraftsCompleted: state.totalCraftsCompleted,
|
||||
comboHitCount: state.comboHitCount,
|
||||
floorHitCount: state.floorHitCount,
|
||||
insight: state.insight,
|
||||
totalInsight: state.totalInsight,
|
||||
prestigeUpgrades: state.prestigeUpgrades,
|
||||
|
||||
@@ -84,6 +84,8 @@ export const createCombatSlice = (
|
||||
let pendingPactOffer = state.pendingPactOffer;
|
||||
const log = [...state.log];
|
||||
const skills = state.skills;
|
||||
let comboHitCount = state.comboHitCount || 0;
|
||||
let floorHitCount = state.floorHitCount || 0;
|
||||
|
||||
const floorElement = getFloorElement(currentFloor);
|
||||
|
||||
@@ -98,15 +100,33 @@ export const createCombatSlice = (
|
||||
let dmg = calcDamage(state, spellId, floorElement);
|
||||
dmg = dmg * effects.baseDamageMultiplier + effects.baseDamageBonus;
|
||||
|
||||
// Increment hit counters
|
||||
comboHitCount += 1;
|
||||
floorHitCount += 1;
|
||||
|
||||
// First Strike: +15% damage on first attack each floor
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.FIRST_STRIKE) && floorHitCount === 1) {
|
||||
dmg *= 1.15;
|
||||
log.unshift('⚡ First Strike! +15% damage!');
|
||||
}
|
||||
|
||||
// Combo Master: Every 5th attack deals 3x damage
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.COMBO_MASTER) && comboHitCount % 5 === 0) {
|
||||
dmg *= 3;
|
||||
log.unshift('🌀 Combo Master! Triple damage!');
|
||||
}
|
||||
|
||||
// Executioner: +100% damage to enemies below 25% HP
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && floorHP / floorMaxHP < 0.25) {
|
||||
dmg *= 2;
|
||||
log.unshift('💀 Executioner! Double damage!');
|
||||
}
|
||||
|
||||
// Berserker: +50% damage when below 50% mana
|
||||
const maxMana = 100; // Would need proper max mana calculation
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
log.unshift('🔥 Berserker! +50% damage!');
|
||||
}
|
||||
|
||||
// Spell echo - chance to cast again
|
||||
@@ -122,6 +142,14 @@ export const createCombatSlice = (
|
||||
|
||||
if (floorHP <= 0) {
|
||||
const wasGuardian = GUARDIANS[currentFloor];
|
||||
|
||||
// Adrenaline Rush: Defeating enemy restores 5% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.ADRENALINE_RUSH)) {
|
||||
const manaRestore = Math.floor(maxMana * 0.05);
|
||||
rawMana = Math.min(rawMana + manaRestore, maxMana);
|
||||
log.unshift(`💚 Adrenaline Rush! Restored ${manaRestore} mana!`);
|
||||
}
|
||||
|
||||
if (wasGuardian && !signedPacts.includes(currentFloor)) {
|
||||
pendingPactOffer = currentFloor;
|
||||
log.unshift(`⚔️ ${wasGuardian.name} defeated! They offer a pact...`);
|
||||
@@ -137,6 +165,7 @@ export const createCombatSlice = (
|
||||
floorHP = floorMaxHP;
|
||||
maxFloorReached = Math.max(maxFloorReached, currentFloor);
|
||||
castProgress = 0;
|
||||
floorHitCount = 0; // Reset floor hit counter for new floor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +181,8 @@ export const createCombatSlice = (
|
||||
pendingPactOffer,
|
||||
castProgress,
|
||||
log,
|
||||
comboHitCount,
|
||||
floorHitCount,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,12 +19,13 @@ export function getEffectiveSkillLevel(
|
||||
}
|
||||
|
||||
export function computeMaxMana(
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers'>,
|
||||
state: Pick<GameState, 'skills' | 'prestigeUpgrades' | 'skillUpgrades' | 'skillTiers' | 'manaHeartBonus'>,
|
||||
effects?: ReturnType<typeof computeEffects>
|
||||
): number {
|
||||
const pu = state.prestigeUpgrades;
|
||||
const skillTiers = state.skillTiers || {};
|
||||
const skillUpgrades = state.skillUpgrades || {};
|
||||
const manaHeartBonus = state.manaHeartBonus || 0;
|
||||
|
||||
const manaWellLevel = getEffectiveSkillLevel(state.skills, 'manaWell', skillTiers);
|
||||
|
||||
@@ -34,7 +35,9 @@ export function computeMaxMana(
|
||||
(pu.manaWell || 0) * 500;
|
||||
|
||||
const computedEffects = effects ?? computeEffects(skillUpgrades, skillTiers);
|
||||
return Math.floor((base + computedEffects.maxManaBonus) * computedEffects.maxManaMultiplier);
|
||||
// Apply MANA_HEART bonus (+10% per loop, compounds)
|
||||
const heartMultiplier = 1 + manaHeartBonus;
|
||||
return Math.floor((base + computedEffects.maxManaBonus) * computedEffects.maxManaMultiplier * heartMultiplier);
|
||||
}
|
||||
|
||||
export function computeElementMax(
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { StateCreator } from 'zustand';
|
||||
import type { GameState, ElementState, SpellCost } from '../types';
|
||||
import { ELEMENTS, MANA_PER_ELEMENT, BASE_UNLOCKED_ELEMENTS } from '../constants';
|
||||
import { computeMaxMana, computeElementMax, computeClickMana, canAffordSpellCost, getMeditationBonus } from './computed';
|
||||
import { computeEffects } from '../upgrade-effects';
|
||||
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from '../upgrade-effects';
|
||||
|
||||
export interface ManaSlice {
|
||||
// State
|
||||
@@ -67,6 +67,11 @@ export const createManaSlice = (
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
const max = computeMaxMana(state, effects);
|
||||
|
||||
// Mana Conversion: Convert 5% of max mana to click bonus
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONVERSION)) {
|
||||
cm += Math.floor(max * 0.05);
|
||||
}
|
||||
|
||||
// Mana Echo: 10% chance to gain double mana from clicks
|
||||
const hasManaEcho = effects.specials?.has('MANA_ECHO') ?? false;
|
||||
if (hasManaEcho && Math.random() < 0.1) {
|
||||
|
||||
@@ -326,6 +326,7 @@ export const useSkillStore = create<SkillState>()(
|
||||
skillProgress: state.skillProgress,
|
||||
skillUpgrades: state.skillUpgrades,
|
||||
skillTiers: state.skillTiers,
|
||||
paidStudySkills: state.paidStudySkills,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import type { GameState } from './types';
|
||||
import { SKILLS_DEF, SPELLS_DEF, getStudyCostMultiplier } from './constants';
|
||||
import { computeEffects, hasSpecial, SPECIAL_EFFECTS } from './upgrade-effects';
|
||||
|
||||
// ─── Study Actions Interface ──────────────────────────────────────────────────
|
||||
|
||||
@@ -39,19 +40,37 @@ export function createStudySlice(
|
||||
|
||||
// Calculate total mana cost and cost per hour
|
||||
const costMult = getStudyCostMultiplier(state.skills);
|
||||
const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
|
||||
let totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
|
||||
|
||||
// CHAIN_STUDY: -5% cost per maxed skill
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.CHAIN_STUDY)) {
|
||||
const maxedSkills = Object.entries(SKILLS_DEF).filter(([id, sk]) =>
|
||||
(state.skills[id] || 0) >= sk.max
|
||||
).length;
|
||||
const discount = Math.pow(0.95, maxedSkills); // -5% per maxed skill
|
||||
totalCost = Math.floor(totalCost * discount);
|
||||
}
|
||||
|
||||
const manaCostPerHour = Math.ceil(totalCost / sk.studyTime);
|
||||
|
||||
// Must have at least 1 hour worth of mana to start
|
||||
if (state.rawMana < manaCostPerHour) return;
|
||||
|
||||
// KNOWLEDGE_TRANSFER: New skills start at 10% progress
|
||||
let initialProgress = state.skillProgress[skillId] || 0;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.KNOWLEDGE_TRANSFER) && initialProgress === 0) {
|
||||
initialProgress = sk.studyTime * 0.10; // 10% of required time
|
||||
log = [`📖 Knowledge Transfer: Starting with 10% progress!`, ...state.log.slice(0, 49)];
|
||||
}
|
||||
|
||||
// Start studying (no upfront cost - mana is deducted per hour during study)
|
||||
set({
|
||||
currentAction: 'study',
|
||||
currentStudyTarget: {
|
||||
type: 'skill',
|
||||
id: skillId,
|
||||
progress: state.skillProgress[skillId] || 0,
|
||||
progress: initialProgress,
|
||||
required: sk.studyTime,
|
||||
manaCostPerHour: manaCostPerHour,
|
||||
totalCost: totalCost,
|
||||
@@ -68,7 +87,18 @@ export function createStudySlice(
|
||||
|
||||
// Calculate total mana cost and cost per hour
|
||||
const costMult = getStudyCostMultiplier(state.skills);
|
||||
const totalCost = Math.floor(sp.unlock * costMult);
|
||||
let totalCost = Math.floor(sp.unlock * costMult);
|
||||
|
||||
// CHAIN_STUDY: -5% cost per maxed skill
|
||||
const effects = computeEffects(state.skillUpgrades || {}, state.skillTiers || {});
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.CHAIN_STUDY)) {
|
||||
const maxedSkills = Object.entries(SKILLS_DEF).filter(([id, sk]) =>
|
||||
(state.skills[id] || 0) >= sk.max
|
||||
).length;
|
||||
const discount = Math.pow(0.95, maxedSkills); // -5% per maxed skill
|
||||
totalCost = Math.floor(totalCost * discount);
|
||||
}
|
||||
|
||||
const studyTime = sp.studyTime || (sp.tier * 4);
|
||||
const manaCostPerHour = Math.ceil(totalCost / studyTime);
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ export interface GameState {
|
||||
rawMana: number;
|
||||
meditateTicks: number;
|
||||
totalManaGathered: number;
|
||||
flowSurgeEndTime: number; // Hour timestamp for FLOW_SURGE effect (0 = inactive)
|
||||
|
||||
// Attunements (class-like system)
|
||||
attunements: Record<string, AttunementState>; // attunement id -> state
|
||||
@@ -141,6 +142,7 @@ export interface GameState {
|
||||
|
||||
// Crafting Progress
|
||||
designProgress: DesignProgress | null;
|
||||
designProgress2: DesignProgress | null; // For ENCHANT_MASTERY (2 concurrent designs)
|
||||
preparationProgress: PreparationProgress | null;
|
||||
applicationProgress: ApplicationProgress | null;
|
||||
equipmentCraftingProgress: EquipmentCraftingProgress | null;
|
||||
@@ -173,6 +175,9 @@ export interface GameState {
|
||||
// Parallel Study Target (for Parallel Mind milestone upgrade)
|
||||
parallelStudyTarget: StudyTarget | null;
|
||||
|
||||
// Study Momentum tracking (for STUDY_MOMENTUM special effect)
|
||||
consecutiveStudyHours: number; // Tracks consecutive hours of studying
|
||||
|
||||
// Golemancy (summoned golems)
|
||||
golemancy: GolemancyState;
|
||||
|
||||
@@ -184,6 +189,10 @@ export interface GameState {
|
||||
totalDamageDealt: number;
|
||||
totalCraftsCompleted: number;
|
||||
|
||||
// Combat special effect tracking
|
||||
comboHitCount: number; // Hit counter for COMBO_MASTER (every 5th attack)
|
||||
floorHitCount: number; // Hit counter for current floor (for FIRST_STRIKE)
|
||||
|
||||
// Prestige
|
||||
insight: number;
|
||||
totalInsight: number;
|
||||
@@ -191,6 +200,9 @@ export interface GameState {
|
||||
memorySlots: number;
|
||||
memories: string[];
|
||||
|
||||
// Mana Well Effects (Phase 4)
|
||||
manaHeartBonus: number; // Cumulative +10% max mana per loop from MANA_HEART
|
||||
|
||||
// Incursion
|
||||
incursionStrength: number;
|
||||
containmentWards: number;
|
||||
|
||||
@@ -56,6 +56,9 @@ export interface ComputedEffects {
|
||||
|
||||
// All active upgrades for display
|
||||
activeUpgrades: ActiveUpgradeEffect[];
|
||||
|
||||
// DEEP_UNDERSTANDING: +10% bonus from all skill levels
|
||||
skillLevelMultiplier: number;
|
||||
}
|
||||
|
||||
// ─── Special Effect IDs ────────────────────────────────────────────────────────
|
||||
@@ -72,7 +75,8 @@ export const SPECIAL_EFFECTS = {
|
||||
ETERNAL_FLOW: 'eternalFlow', // Regen immune to all penalties
|
||||
|
||||
// Mana Well special effects
|
||||
DESPERATE_WELLS: 'desperateWells', // +50% regen when below 25% mana
|
||||
DESPAIR_WELLS: 'despairWells', // +50% regen when below 25% mana (task name)
|
||||
DESPERATE_WELLS: 'desperateWells', // +50% regen when below 25% mana (legacy name)
|
||||
MANA_ECHO: 'manaEcho', // 10% chance double mana from clicks
|
||||
EMERGENCY_RESERVE: 'emergencyReserve', // Keep 10% mana on new loop
|
||||
MANA_THRESHOLD: 'manaThreshold', // +30% max mana, -10% regen trade-off
|
||||
@@ -230,8 +234,14 @@ export function computeEffects(
|
||||
permanentRegenBonus: 0,
|
||||
specials: new Set<string>(),
|
||||
activeUpgrades,
|
||||
skillLevelMultiplier: 1,
|
||||
};
|
||||
|
||||
// Apply DEEP_UNDERSTANDING: +10% bonus from all skill levels
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.DEEP_UNDERSTANDING)) {
|
||||
effects.skillLevelMultiplier = 1.10;
|
||||
}
|
||||
|
||||
// Apply each upgrade effect
|
||||
for (const upgrade of activeUpgrades) {
|
||||
const { effect } = upgrade;
|
||||
@@ -309,6 +319,12 @@ export function computeEffects(
|
||||
}
|
||||
}
|
||||
|
||||
// MANA_THRESHOLD: +30% max mana, -10% regen trade-off
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_THRESHOLD)) {
|
||||
effects.maxManaMultiplier *= 1.30;
|
||||
effects.regenMultiplier *= 0.90;
|
||||
}
|
||||
|
||||
return effects;
|
||||
}
|
||||
|
||||
@@ -336,16 +352,47 @@ export function computeDynamicRegen(
|
||||
regen += Math.floor(maxMana / 100) * 0.1;
|
||||
}
|
||||
|
||||
// Mana Waterfall: +0.25 regen per 100 max mana (upgraded cascade)
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_WATERFALL)) {
|
||||
regen += Math.floor(maxMana / 100) * 0.25;
|
||||
}
|
||||
|
||||
// Mana Torrent: +50% regen when above 75% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TORRENT) && currentMana > maxMana * 0.75) {
|
||||
regen *= 1.5;
|
||||
}
|
||||
|
||||
// Desperate Wells: +50% regen when below 25% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) && currentMana < maxMana * 0.25) {
|
||||
// Desperate Wells / Despair Wells: +50% regen when below 25% mana
|
||||
if ((hasSpecial(effects, SPECIAL_EFFECTS.DESPERATE_WELLS) || hasSpecial(effects, SPECIAL_EFFECTS.DESPAIR_WELLS)) && currentMana < maxMana * 0.25) {
|
||||
regen *= 1.5;
|
||||
}
|
||||
|
||||
// Panic Reserve: +100% regen when below 10% mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.PANIC_RESERVE) && currentMana < maxMana * 0.1) {
|
||||
regen *= 2.0;
|
||||
}
|
||||
|
||||
// Deep Reserve: +0.5 regen per 100 max mana
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.DEEP_RESERVE)) {
|
||||
regen += Math.floor(maxMana / 100) * 0.5;
|
||||
}
|
||||
|
||||
// Mana Core: 0.5% of max mana added as regen
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_CORE)) {
|
||||
regen += maxMana * 0.005;
|
||||
}
|
||||
|
||||
// Mana Tide: Regen pulses ±50% (sinusoidal based on time)
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.MANA_TIDE)) {
|
||||
const pulseFactor = 0.5 + 0.5 * Math.sin(Date.now() / 10000); // 10 second cycles
|
||||
regen *= (0.5 + pulseFactor * 0.5); // Range: 0.5x to 1.0x
|
||||
}
|
||||
|
||||
// Eternal Flow: Regen immune to ALL penalties (stronger than Steady Stream)
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.ETERNAL_FLOW)) {
|
||||
return regen * effects.regenMultiplier;
|
||||
}
|
||||
|
||||
// Steady Stream: Regen immune to incursion
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.STEADY_STREAM)) {
|
||||
return regen * effects.regenMultiplier;
|
||||
@@ -370,6 +417,12 @@ export function computeDynamicClickMana(
|
||||
// Note: The chance is handled in the click handler, this just returns the base
|
||||
// The click handler should check hasSpecial and apply the 10% chance
|
||||
|
||||
// Mana Genesis: Generate 1% of max mana per hour passively
|
||||
// This is handled in the game loop (store.ts), not here
|
||||
|
||||
// Mana Heart: +10% max mana per loop (permanent)
|
||||
// This is applied during loop reset in store.ts
|
||||
|
||||
return Math.floor((clickMana + effects.clickManaBonus) * effects.clickManaMultiplier);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user