cleanup: delete computed-stats.ts shim and store/index.ts
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 57s

- Delete src/lib/game/computed-stats.ts (root-level re-export shim)
- Delete src/lib/game/store/index.ts (nothing imports from it)
- Update __tests__/computed-stats.test.ts to import from ../utils instead
- Clean up craftingStore.ts imports (remove unused useGameStore, CraftingApply)

Typecheck and lint pass (pre-existing DisciplinesTab.tsx errors unchanged)
This commit is contained in:
2026-05-18 12:08:38 +02:00
parent 20c2ebd7b5
commit 2805f75f5e
54 changed files with 333 additions and 2936 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# Circular Dependencies # Circular Dependencies
Generated: 2026-05-18T08:33:19.846Z Generated: 2026-05-18T09:26:29.031Z
Found: 7 circular chain(s) — these MUST be fixed before modifying involved files. Found: 7 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 151 files (1.4s) (37 warnings) 1. Processed 151 files (1.5s) (37 warnings)
2. 1) data/equipment/index.ts > data/equipment/utils.ts 2. 1) data/equipment/index.ts > data/equipment/utils.ts
3. 2) data/golems/index.ts > data/golems/utils.ts 3. 2) data/golems/index.ts > data/golems/utils.ts
4. 3) stores/combat-actions.ts > stores/combatStore.ts 4. 3) stores/combat-actions.ts > stores/combatStore.ts
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"_meta": { "_meta": {
"generated": "2026-05-18T08:33:18.234Z", "generated": "2026-05-18T09:26:27.302Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
}, },
+1 -25
View File
@@ -78,7 +78,6 @@ Mana-Loop/
│ │ │ │ ├── index.tsx │ │ │ │ ├── index.tsx
│ │ │ │ └── types.ts │ │ │ │ └── types.ts
│ │ │ ├── StatsTab/ │ │ │ ├── StatsTab/
│ │ │ │ ├── ActiveUpgradesSection.tsx
│ │ │ │ ├── CombatStatsSection.tsx │ │ │ │ ├── CombatStatsSection.tsx
│ │ │ │ ├── ElementStatsSection.tsx │ │ │ │ ├── ElementStatsSection.tsx
│ │ │ │ ├── LoopStatsSection.tsx │ │ │ │ ├── LoopStatsSection.tsx
@@ -105,20 +104,8 @@ Mana-Loop/
│ │ │ │ ├── GolemDebug.tsx │ │ │ │ ├── GolemDebug.tsx
│ │ │ │ ├── PactDebug.tsx │ │ │ │ ├── PactDebug.tsx
│ │ │ │ └── index.tsx │ │ │ │ └── index.tsx
│ │ │ ├── layout/
│ │ │ │ ├── Header.tsx
│ │ │ │ └── TabBar.tsx
│ │ │ ├── shared/ │ │ │ ├── shared/
│ │ │ │ ── MemorySlotPicker.tsx │ │ │ │ ── MemorySlotPicker.tsx
│ │ │ │ ├── StudyProgress.tsx
│ │ │ │ └── UpgradeDialog.tsx
│ │ │ ├── stats/
│ │ │ │ ├── CombatStatsSection.tsx
│ │ │ │ ├── ManaStatsSection.tsx
│ │ │ │ ├── ManaTypeBreakdown.tsx
│ │ │ │ ├── StudyStatsSection.tsx
│ │ │ │ ├── UpgradeEffectsSection.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── tabs/ │ │ │ ├── tabs/
│ │ │ │ └── DisciplinesTab.tsx │ │ │ │ └── DisciplinesTab.tsx
│ │ │ ├── AchievementsDisplay.tsx │ │ │ ├── AchievementsDisplay.tsx
@@ -178,11 +165,6 @@ Mana-Loop/
│ │ │ ├── store-method-tests/ │ │ │ ├── store-method-tests/
│ │ │ ├── bug-fixes.test.ts │ │ │ ├── bug-fixes.test.ts
│ │ │ └── computed-stats.test.ts │ │ │ └── computed-stats.test.ts
│ │ ├── attunements/
│ │ │ ├── data.ts
│ │ │ ├── index.ts
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── constants/ │ │ ├── constants/
│ │ │ ├── spells-modules/ │ │ │ ├── spells-modules/
│ │ │ │ ├── advanced-spells.ts │ │ │ │ ├── advanced-spells.ts
@@ -280,7 +262,6 @@ Mana-Loop/
│ │ │ ├── combatSlice.ts │ │ │ ├── combatSlice.ts
│ │ │ ├── computed.ts │ │ │ ├── computed.ts
│ │ │ ├── craftingSlice.ts │ │ │ ├── craftingSlice.ts
│ │ │ ├── index.ts
│ │ │ ├── manaSlice.ts │ │ │ ├── manaSlice.ts
│ │ │ ├── pactSlice.ts │ │ │ ├── pactSlice.ts
│ │ │ ├── prestigeSlice.ts │ │ │ ├── prestigeSlice.ts
@@ -328,7 +309,6 @@ Mana-Loop/
│ │ │ ├── index.ts │ │ │ ├── index.ts
│ │ │ ├── mana-utils.ts │ │ │ ├── mana-utils.ts
│ │ │ └── room-utils.ts │ │ │ └── room-utils.ts
│ │ ├── computed-stats.ts
│ │ ├── constants.ts │ │ ├── constants.ts
│ │ ├── crafting-apply.ts │ │ ├── crafting-apply.ts
│ │ ├── crafting-attunements.ts │ │ ├── crafting-attunements.ts
@@ -341,14 +321,10 @@ Mana-Loop/
│ │ ├── debug-context.tsx │ │ ├── debug-context.tsx
│ │ ├── dynamic-compute.ts │ │ ├── dynamic-compute.ts
│ │ ├── effects.ts │ │ ├── effects.ts
│ │ ├── effects.ts.fix
│ │ ├── formatting.ts
│ │ ├── navigation-slice.ts
│ │ ├── special-effects.ts │ │ ├── special-effects.ts
│ │ ├── store.test.ts │ │ ├── store.test.ts
│ │ ├── store.ts │ │ ├── store.ts
│ │ ├── stores.test.ts │ │ ├── stores.test.ts
│ │ ├── study-slice.ts
│ │ ├── types.ts │ │ ├── types.ts
│ │ ├── upgrade-effects.ts │ │ ├── upgrade-effects.ts
│ │ └── upgrade-effects.types.ts │ │ └── upgrade-effects.types.ts
+8 -13
View File
@@ -9,7 +9,7 @@ import { ActionButtons } from '@/components/game';
import { AttunementStatus } from '@/components/game/AttunementStatus'; import { AttunementStatus } from '@/components/game/AttunementStatus';
import { ActivityLogPanel } from '@/components/game/ActivityLogPanel'; import { ActivityLogPanel } from '@/components/game/ActivityLogPanel';
import { DebugName } from '@/lib/game/debug-context'; import { DebugName } from '@/lib/game/debug-context';
import { useGameStore, useManaStore, useSkillStore, useCombatStore, useCraftingStore, usePrestigeStore } from '@/lib/game/stores'; import { useGameStore, useManaStore, useCombatStore, useCraftingStore, usePrestigeStore } from '@/lib/game/stores';
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores'; import { getMeditationBonus, getIncursionStrength } from '@/lib/game/stores';
import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects'; import { computeTotalMaxMana, computeTotalRegen, computeTotalClickMana } from '@/lib/game/effects';
@@ -20,9 +20,6 @@ export function LeftPanel() {
const rawMana = useManaStore((s) => s.rawMana); const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements); const elements = useManaStore((s) => s.elements);
const meditateTicks = useManaStore((s) => s.meditateTicks); const meditateTicks = useManaStore((s) => s.meditateTicks);
const skills = useSkillStore((s) => s.skills);
const skillTiers = useSkillStore((s) => s.skillTiers);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
@@ -30,7 +27,6 @@ export function LeftPanel() {
const spireMode = useCombatStore((s) => s.spireMode); const spireMode = useCombatStore((s) => s.spireMode);
const enterSpireMode = useCombatStore((s) => s.enterSpireMode); const enterSpireMode = useCombatStore((s) => s.enterSpireMode);
const currentAction = useCombatStore((s) => s.currentAction); const currentAction = useCombatStore((s) => s.currentAction);
const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
const designProgress = useCraftingStore((s) => s.designProgress); const designProgress = useCraftingStore((s) => s.designProgress);
const designProgress2 = useCraftingStore((s) => s.designProgress2); const designProgress2 = useCraftingStore((s) => s.designProgress2);
const preparationProgress = useCraftingStore((s) => s.preparationProgress); const preparationProgress = useCraftingStore((s) => s.preparationProgress);
@@ -56,11 +52,11 @@ export function LeftPanel() {
return () => cancelAnimationFrame(animationFrameId); return () => cancelAnimationFrame(animationFrameId);
}, [isGathering, gatherMana]); }, [isGathering, gatherMana]);
const upgradeEffects = getUnifiedEffects({ skillUpgrades, skillTiers, equippedInstances, equipmentInstances }); const upgradeEffects = getUnifiedEffects({ skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances });
const maxMana = computeTotalMaxMana({ skills, prestigeUpgrades, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, upgradeEffects); const maxMana = computeTotalMaxMana({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const baseRegen = computeTotalRegen({ skills, prestigeUpgrades, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, upgradeEffects); const baseRegen = computeTotalRegen({ skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const clickMana = computeTotalClickMana({ skills, skillUpgrades, skillTiers, equippedInstances, equipmentInstances }, upgradeEffects); const clickMana = computeTotalClickMana({ skills: {}, skillUpgrades: {}, skillTiers: {}, equippedInstances, equipmentInstances }, upgradeEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency); const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour)); const incursionStrength = getIncursionStrength(useGameStore((s) => s.day), useGameStore((s) => s.hour));
const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier; const effectiveRegen = baseRegen * (1 - incursionStrength) * meditationMultiplier;
@@ -84,7 +80,7 @@ export function LeftPanel() {
{/* 2. Spire Entry */} {/* 2. Spire Entry */}
{!spireMode && ( {!spireMode && (
<DebugName name="ClimbSpireButton"> <DebugName name="ClimbSpireButton">
<Button className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700 text-white" size="lg" onClick={enterSpireMode}> <Button className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-600 text-white" size="lg" onClick={enterSpireMode}>
<Mountain className="w-5 h-5 mr-2" /> <Mountain className="w-5 h-5 mr-2" />
Climb the Spire Climb the Spire
</Button> </Button>
@@ -98,7 +94,6 @@ export function LeftPanel() {
<CardContent className="pt-3"> <CardContent className="pt-3">
<ActionButtons <ActionButtons
currentAction={currentAction} currentAction={currentAction}
currentStudyTarget={currentStudyTarget as any}
designProgress={designProgress} designProgress={designProgress}
designProgress2={designProgress2} designProgress2={designProgress2}
preparationProgress={preparationProgress} preparationProgress={preparationProgress}
@@ -127,4 +122,4 @@ export function LeftPanel() {
</DebugName> </DebugName>
</div> </div>
); );
} }
+17 -41
View File
@@ -1,14 +1,12 @@
'use client'; 'use client';
import { useEffect, useState, lazy, Suspense } from 'react'; import { useEffect, useState, lazy, Suspense } from 'react';
import type { JSX } from 'react';
// Import from new modular stores // Import from new modular stores
import { import {
useGameStore, useGameStore,
useUIStore, useUIStore,
useManaStore, useManaStore,
useSkillStore,
useCombatStore, useCombatStore,
usePrestigeStore, usePrestigeStore,
useCraftingStore, useCraftingStore,
@@ -21,23 +19,16 @@ import {
} from '@/lib/game/stores'; } from '@/lib/game/stores';
import { useGameLoop } from '@/lib/game/stores/gameHooks'; import { useGameLoop } from '@/lib/game/stores/gameHooks';
import { getUnifiedEffects } from '@/lib/game/effects'; import { getUnifiedEffects } from '@/lib/game/effects';
import { import { SPELLS_DEF } from '@/lib/game/constants';
getStudySpeedMultiplier,
getStudyCostMultiplier,
SPELLS_DEF,
ELEMENTS,
GUARDIANS,
} from '@/lib/game/constants';
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
import { TimeDisplay } from '@/components/game'; import { TimeDisplay } from '@/components/game';
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects'; import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; 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 { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { RotateCcw, Mountain } from 'lucide-react';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
import { ErrorBoundary } from '@/components/ErrorBoundary'; import { ErrorBoundary } from '@/components/ErrorBoundary';
import { DebugName } from '@/lib/game/debug-context'; import { DebugName } from '@/lib/game/debug-context';
@@ -48,7 +39,6 @@ import { LeftPanel } from './components/LeftPanel';
// Lazy load tab components // Lazy load tab components
const SpireTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireTab }))); 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 SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab })));
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab }))); const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab })));
@@ -143,10 +133,6 @@ export default function ManaLoopGame() {
const initGame = useGameStore((s) => s.initGame); const initGame = useGameStore((s) => s.initGame);
useGameLoop(); useGameLoop();
const skills = useSkillStore((s) => s.skills);
const skillTiers = useSkillStore((s) => s.skillTiers);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const insight = usePrestigeStore((s) => s.insight); const insight = usePrestigeStore((s) => s.insight);
const loopInsight = usePrestigeStore((s) => s.loopInsight); const loopInsight = usePrestigeStore((s) => s.loopInsight);
@@ -154,8 +140,7 @@ export default function ManaLoopGame() {
const rawMana = useManaStore((s) => s.rawMana); const rawMana = useManaStore((s) => s.rawMana);
const meditateTicks = useManaStore((s) => s.meditateTicks); const meditateTicks = useManaStore((s) => s.meditateTicks);
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
const spells = useCombatStore((s) => s.spells);
const spireMode = useCombatStore((s) => s.spireMode); const spireMode = useCombatStore((s) => s.spireMode);
const gameOver = useUIStore((s) => s.gameOver); const gameOver = useUIStore((s) => s.gameOver);
@@ -166,34 +151,34 @@ export default function ManaLoopGame() {
// Derived state // Derived state
const upgradeEffects = getUnifiedEffects({ const upgradeEffects = getUnifiedEffects({
skillUpgrades, skillUpgrades: {},
skillTiers, skillTiers: {},
equippedInstances, equippedInstances,
equipmentInstances equipmentInstances
}); });
const maxMana = computeMaxMana({ const maxMana = computeMaxMana({
skills, skills: {},
prestigeUpgrades, prestigeUpgrades,
skillUpgrades, skillUpgrades: {},
skillTiers skillTiers: {}
}, upgradeEffects); }, upgradeEffects);
const baseRegen = computeRegen({ const baseRegen = computeRegen({
skills, skills: {},
prestigeUpgrades, prestigeUpgrades,
skillUpgrades, skillUpgrades: {},
skillTiers skillTiers: {}
}, upgradeEffects); }, upgradeEffects);
const clickMana = computeClickMana({ const clickMana = computeClickMana({
skills, skills: {},
prestigeUpgrades, prestigeUpgrades,
skillUpgrades, skillUpgrades: {},
skillTiers skillTiers: {}
}); });
const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency); const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(day, hour); const incursionStrength = getIncursionStrength(day, hour);
// Effective regen with incursion penalty // Effective regen with incursion penalty
@@ -258,7 +243,6 @@ export default function ManaLoopGame() {
<TabsTrigger value="spire" className="text-xs px-2 py-1"> Spire</TabsTrigger> <TabsTrigger value="spire" className="text-xs px-2 py-1"> Spire</TabsTrigger>
<TabsTrigger value="attunements" className="text-xs px-2 py-1"> Attune</TabsTrigger> <TabsTrigger value="attunements" className="text-xs px-2 py-1"> Attune</TabsTrigger>
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golems</TabsTrigger> <TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golems</TabsTrigger>
<TabsTrigger value="skills" className="text-xs px-2 py-1">📚 Skills</TabsTrigger>
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger> <TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
<TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡 Gear</TabsTrigger> <TabsTrigger value="equipment" className="text-xs px-2 py-1">🛡 Gear</TabsTrigger>
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger> <TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
@@ -294,14 +278,6 @@ export default function ManaLoopGame() {
</ErrorBoundary> </ErrorBoundary>
</TabsContent> </TabsContent>
<TabsContent value="skills">
<ErrorBoundary fallback={<div className="p-4 text-red-400">skills tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<SkillsTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="spells"> <TabsContent value="spells">
<ErrorBoundary fallback={<div className="p-4 text-red-400">spells tab failed to load.</div>}> <ErrorBoundary fallback={<div className="p-4 text-red-400">spells tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}> <Suspense fallback={<TabLoadingFallback />}>
+1 -1
View File
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Target, FlaskConical, Sparkles, Play, Pause, X } from 'lucide-react'; import { Target, FlaskConical, Sparkles, Play, Pause, X } from 'lucide-react';
import { fmt } from '@/lib/game/stores'; import { fmt } from '@/lib/game/stores';
import { formatStudyTime } from '@/lib/game/formatting'; import { formatStudyTime } from '@/lib/game/utils/formatting';
import type { EquipmentInstance, EnchantmentDesign } from '@/lib/game/types'; import type { EquipmentInstance, EnchantmentDesign } from '@/lib/game/types';
interface CraftingProgressProps { interface CraftingProgressProps {
+36 -68
View File
@@ -1,7 +1,6 @@
'use client'; 'use client';
import { useMemo, type ReactNode } from 'react'; import { useMemo, type ReactNode } from 'react';
import { useSkillStore } from '@/lib/game/stores/skillStore';
import { useManaStore } from '@/lib/game/stores/manaStore'; import { useManaStore } from '@/lib/game/stores/manaStore';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore'; import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import { useUIStore } from '@/lib/game/stores/uiStore'; import { useUIStore } from '@/lib/game/stores/uiStore';
@@ -9,7 +8,6 @@ import { useCombatStore } from '@/lib/game/stores/combatStore';
import { useGameStore } from '@/lib/game/stores/gameStore'; import { useGameStore } from '@/lib/game/stores/gameStore';
import { computeEffects } from '@/lib/game/upgrade-effects'; import { computeEffects } from '@/lib/game/upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/special-effects'; import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/special-effects';
import { getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/constants';
import { import {
computeMaxMana, computeMaxMana,
computeRegen, computeRegen,
@@ -32,7 +30,6 @@ import { GameContext } from './context-create';
function createUnifiedStore( function createUnifiedStore(
gameStore: ReturnType<typeof useGameStore.getState>, gameStore: ReturnType<typeof useGameStore.getState>,
skillState: ReturnType<typeof useSkillStore.getState>,
manaState: ReturnType<typeof useManaStore.getState>, manaState: ReturnType<typeof useManaStore.getState>,
prestigeState: ReturnType<typeof usePrestigeStore.getState>, prestigeState: ReturnType<typeof usePrestigeStore.getState>,
uiState: ReturnType<typeof useUIStore.getState>, uiState: ReturnType<typeof useUIStore.getState>,
@@ -49,7 +46,7 @@ function createUnifiedStore(
resetGame: gameStore.resetGame, resetGame: gameStore.resetGame,
gatherMana: gameStore.gatherMana, gatherMana: gameStore.gatherMana,
startNewLoop: gameStore.startNewLoop, startNewLoop: gameStore.startNewLoop,
// From manaStore // From manaStore
rawMana: manaState.rawMana, rawMana: manaState.rawMana,
meditateTicks: manaState.meditateTicks, meditateTicks: manaState.meditateTicks,
@@ -61,25 +58,7 @@ function createUnifiedStore(
convertMana: manaState.convertMana, convertMana: manaState.convertMana,
unlockElement: manaState.unlockElement, unlockElement: manaState.unlockElement,
craftComposite: manaState.craftComposite, craftComposite: manaState.craftComposite,
// From skillStore
skills: skillState.skills,
skillProgress: skillState.skillProgress,
skillUpgrades: skillState.skillUpgrades,
skillTiers: skillState.skillTiers,
paidStudySkills: skillState.paidStudySkills,
currentStudyTarget: skillState.currentStudyTarget,
parallelStudyTarget: skillState.parallelStudyTarget,
setSkillLevel: skillState.setSkillLevel,
startStudyingSkill: skillState.startStudyingSkill,
startStudyingSpell: skillState.startStudyingSpell,
cancelStudy: skillState.cancelStudy,
selectSkillUpgrade: skillState.selectSkillUpgrade,
deselectSkillUpgrade: skillState.deselectSkillUpgrade,
commitSkillUpgrades: skillState.commitSkillUpgrades,
tierUpSkill: skillState.tierUpSkill,
getSkillUpgradeChoices: skillState.getSkillUpgradeChoices,
// From prestigeStore // From prestigeStore
loopCount: prestigeState.loopCount, loopCount: prestigeState.loopCount,
insight: prestigeState.insight, insight: prestigeState.insight,
@@ -101,7 +80,7 @@ function createUnifiedStore(
cancelPactRitual: prestigeState.cancelPactRitual, cancelPactRitual: prestigeState.cancelPactRitual,
removePact: prestigeState.removePact, removePact: prestigeState.removePact,
defeatGuardian: prestigeState.defeatGuardian, defeatGuardian: prestigeState.defeatGuardian,
// From combatStore // From combatStore
currentFloor: combatState.currentFloor, currentFloor: combatState.currentFloor,
floorHP: combatState.floorHP, floorHP: combatState.floorHP,
@@ -115,7 +94,7 @@ function createUnifiedStore(
setSpell: combatState.setSpell, setSpell: combatState.setSpell,
learnSpell: combatState.learnSpell, learnSpell: combatState.learnSpell,
advanceFloor: combatState.advanceFloor, advanceFloor: combatState.advanceFloor,
// From uiStore // From uiStore
log: uiState.logs, log: uiState.logs,
paused: uiState.paused, paused: uiState.paused,
@@ -131,125 +110,114 @@ function createUnifiedStore(
export function GameProvider({ children }: { children: ReactNode }) { export function GameProvider({ children }: { children: ReactNode }) {
// Get all individual stores // Get all individual stores
const gameStore = useGameStore(); const gameStore = useGameStore();
const skillState = useSkillStore();
const manaState = useManaStore(); const manaState = useManaStore();
const prestigeState = usePrestigeStore(); const prestigeState = usePrestigeStore();
const uiState = useUIStore(); const uiState = useUIStore();
const combatState = useCombatStore(); const combatState = useCombatStore();
// Create unified store object for backward compatibility // Create unified store object for backward compatibility
const unifiedStore = useMemo( const unifiedStore = useMemo(
() => createUnifiedStore(gameStore, skillState, manaState, prestigeState, uiState, combatState), () => createUnifiedStore(gameStore, manaState, prestigeState, uiState, combatState),
[gameStore, skillState, manaState, prestigeState, uiState, combatState] [gameStore, manaState, prestigeState, uiState, combatState]
); );
// Computed effects from upgrades // Computed effects from upgrades
const upgradeEffects = useMemo( const upgradeEffects = useMemo(
() => computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {}), () => computeEffects({}, {}),
[skillState.skillUpgrades, skillState.skillTiers] []
); );
// Create a minimal state object for compute functions // Create a minimal state object for compute functions
const stateForCompute = useMemo(() => ({ const stateForCompute = useMemo(() => ({
skills: skillState.skills,
prestigeUpgrades: prestigeState.prestigeUpgrades, prestigeUpgrades: prestigeState.prestigeUpgrades,
skillUpgrades: skillState.skillUpgrades,
skillTiers: skillState.skillTiers,
signedPacts: prestigeState.signedPacts, signedPacts: prestigeState.signedPacts,
rawMana: manaState.rawMana, rawMana: manaState.rawMana,
meditateTicks: manaState.meditateTicks, meditateTicks: manaState.meditateTicks,
incursionStrength: gameStore.incursionStrength, incursionStrength: gameStore.incursionStrength,
}), [skillState, prestigeState, manaState, gameStore.incursionStrength]); }), [prestigeState, manaState, gameStore.incursionStrength]);
// Derived stats // Derived stats
const maxMana = useMemo( const maxMana = useMemo(
() => computeMaxMana(stateForCompute, upgradeEffects), () => computeMaxMana(stateForCompute, upgradeEffects),
[stateForCompute, upgradeEffects] [stateForCompute, upgradeEffects]
); );
const baseRegen = useMemo( const baseRegen = useMemo(
() => computeRegen(stateForCompute, upgradeEffects), () => computeRegen(stateForCompute, upgradeEffects),
[stateForCompute, upgradeEffects] [stateForCompute, upgradeEffects]
); );
const clickMana = useMemo(() => computeClickMana(stateForCompute), [stateForCompute]); const clickMana = useMemo(() => computeClickMana(stateForCompute), [stateForCompute]);
// Floor element from combat store // Floor element from combat store
const floorElem = useMemo(() => getFloorElement(combatState.currentFloor), [combatState.currentFloor]); const floorElem = useMemo(() => getFloorElement(combatState.currentFloor), [combatState.currentFloor]);
const floorElemDef = ELEMENTS[floorElem]; const floorElemDef = ELEMENTS[floorElem];
const isGuardianFloor = !!GUARDIANS[combatState.currentFloor]; const isGuardianFloor = !!GUARDIANS[combatState.currentFloor];
const currentGuardian = GUARDIANS[combatState.currentFloor]; const currentGuardian = GUARDIANS[combatState.currentFloor];
const activeSpellDef = SPELLS_DEF[combatState.activeSpell]; const activeSpellDef = SPELLS_DEF[combatState.activeSpell];
const meditationMultiplier = useMemo( const meditationMultiplier = useMemo(
() => getMeditationBonus(manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency), () => getMeditationBonus(manaState.meditateTicks, {}, upgradeEffects.meditationEfficiency),
[manaState.meditateTicks, skillState.skills, upgradeEffects.meditationEfficiency] [manaState.meditateTicks, upgradeEffects.meditationEfficiency]
); );
const incursionStrength = useMemo( const incursionStrength = useMemo(
() => getIncursionStrength(gameStore.day, gameStore.hour), () => getIncursionStrength(gameStore.day, gameStore.hour),
[gameStore.day, gameStore.hour] [gameStore.day, gameStore.hour]
); );
const studySpeedMult = useMemo( const studySpeedMult = 1;
() => getStudySpeedMultiplier(skillState.skills),
[skillState.skills] const studyCostMult = 1;
);
const studyCostMult = useMemo(
() => getStudyCostMultiplier(skillState.skills),
[skillState.skills]
);
// Effective regen calculations // Effective regen calculations
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE) const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
? Math.floor(maxMana / 100) * 0.1 ? Math.floor(maxMana / 100) * 0.1
: 0; : 0;
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL) const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
? Math.floor(maxMana / 100) * 0.25 ? Math.floor(maxMana / 100) * 0.25
: 0; : 0;
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier; const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
// Has special flags for UI // Has special flags for UI
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL); const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE); const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW); const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW); const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
// Active boons // Active boons
const activeBoons = useMemo( const activeBoons = useMemo(
() => getBoonBonuses(prestigeState.signedPacts), () => getBoonBonuses(prestigeState.signedPacts),
[prestigeState.signedPacts] [prestigeState.signedPacts]
); );
// DPS calculation - based on active spell, attack speed, and damage // DPS calculation - based on active spell, attack speed, and damage
const dps = useMemo(() => { const dps = useMemo(() => {
if (!activeSpellDef) return 0; if (!activeSpellDef) return 0;
const baseDmg = calcDamage( const baseDmg = calcDamage(
{ skills: skillState.skills, signedPacts: prestigeState.signedPacts }, { signedPacts: prestigeState.signedPacts },
combatState.activeSpell, combatState.activeSpell,
floorElem floorElem
); );
const dmgWithEffects = baseDmg * upgradeEffects.baseDamageMultiplier + upgradeEffects.baseDamageBonus; const dmgWithEffects = baseDmg * upgradeEffects.baseDamageMultiplier + upgradeEffects.baseDamageBonus;
const attackSpeed = (1 + (skillState.skills.quickCast || 0) * 0.05) * upgradeEffects.attackSpeedMultiplier; const attackSpeed = (1 + 0 * 0.05) * upgradeEffects.attackSpeedMultiplier;
const castSpeed = activeSpellDef.castSpeed || 1; const castSpeed = activeSpellDef.castSpeed || 1;
return dmgWithEffects * attackSpeed * castSpeed; return dmgWithEffects * attackSpeed * castSpeed;
}, [activeSpellDef, skillState.skills, prestigeState.signedPacts, floorElem, upgradeEffects, combatState.activeSpell]); }, [activeSpellDef, prestigeState.signedPacts, floorElem, upgradeEffects, combatState.activeSpell]);
// Helper functions // Helper functions
const canCastSpell = (spellId: string): boolean => { const canCastSpell = (spellId: string): boolean => {
const spell = SPELLS_DEF[spellId]; const spell = SPELLS_DEF[spellId];
if (!spell) return false; if (!spell) return false;
return canAffordSpellCost(spell.cost, manaState.rawMana, manaState.elements); return canAffordSpellCost(spell.cost, manaState.rawMana, manaState.elements);
}; };
const value: GameContextValue = { const value: GameContextValue = {
store: unifiedStore, store: unifiedStore,
skillStore: skillState,
manaStore: manaState, manaStore: manaState,
prestigeStore: prestigeState, prestigeStore: prestigeState,
uiStore: uiState, uiStore: uiState,
+12 -41
View File
@@ -1,5 +1,4 @@
import type { ElementDef, GuardianDef, SpellDef, GameAction } from '@/lib/game/types'; import type { ElementDef, GuardianDef, SpellDef, GameAction } from '@/lib/game/types';
import { useSkillStore } from '@/lib/game/stores/skillStore';
import { useManaStore } from '@/lib/game/stores/manaStore'; import { useManaStore } from '@/lib/game/stores/manaStore';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore'; import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import { useUIStore } from '@/lib/game/stores/uiStore'; import { useUIStore } from '@/lib/game/stores/uiStore';
@@ -20,7 +19,7 @@ export interface UnifiedStore {
resetGame: () => void; resetGame: () => void;
gatherMana: () => void; gatherMana: () => void;
startNewLoop: () => void; startNewLoop: () => void;
// From manaStore // From manaStore
rawMana: number; rawMana: number;
meditateTicks: number; meditateTicks: number;
@@ -32,34 +31,7 @@ export interface UnifiedStore {
convertMana: (element: string, amount: number) => boolean; convertMana: (element: string, amount: number) => boolean;
unlockElement: (element: string, cost: number) => boolean; unlockElement: (element: string, cost: number) => boolean;
craftComposite: (target: string, recipe: string[]) => boolean; craftComposite: (target: string, recipe: string[]) => boolean;
// From skillStore
skills: Record<string, number>;
skillProgress: Record<string, number>;
skillUpgrades: Record<string, string[]>;
skillTiers: Record<string, number>;
paidStudySkills: Record<string, number>;
currentStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
parallelStudyTarget: { type: 'skill' | 'spell' | 'blueprint'; id: string; progress: number; required: number } | null;
setSkillLevel: (skillId: string, level: number) => void;
startStudyingSkill: (skillId: string, rawMana: number) => { started: boolean; cost: number };
startStudyingSpell: (spellId: string, rawMana: number, studyTime: number) => { started: boolean; cost: number };
cancelStudy: (retentionBonus: number) => void;
selectSkillUpgrade: (skillId: string, upgradeId: string) => void;
deselectSkillUpgrade: (skillId: string, upgradeId: string) => void;
commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void;
tierUpSkill: (skillId: string) => void;
getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => {
available: Array<{
id: string;
name: string;
desc: string;
milestone: 5 | 10;
effect: { type: string; stat?: string; value?: number; specialId?: string }
}>;
selected: string[]
};
// From prestigeStore // From prestigeStore
loopCount: number; loopCount: number;
insight: number; insight: number;
@@ -81,7 +53,7 @@ export interface UnifiedStore {
cancelPactRitual: () => void; cancelPactRitual: () => void;
removePact: (floor: number) => void; removePact: (floor: number) => void;
defeatGuardian: (floor: number) => void; defeatGuardian: (floor: number) => void;
// From combatStore // From combatStore
currentFloor: number; currentFloor: number;
floorHP: number; floorHP: number;
@@ -95,7 +67,7 @@ export interface UnifiedStore {
setSpell: (spellId: string) => void; setSpell: (spellId: string) => void;
learnSpell: (spellId: string) => void; learnSpell: (spellId: string) => void;
advanceFloor: () => void; advanceFloor: () => void;
// From uiStore // From uiStore
log: string[]; log: string[];
paused: boolean; paused: boolean;
@@ -110,17 +82,16 @@ export interface UnifiedStore {
export interface GameContextValue { export interface GameContextValue {
// Unified store for backward compatibility // Unified store for backward compatibility
store: UnifiedStore; store: UnifiedStore;
// Individual stores for direct access if needed // Individual stores for direct access if needed
skillStore: ReturnType<typeof useSkillStore.getState>;
manaStore: ReturnType<typeof useManaStore.getState>; manaStore: ReturnType<typeof useManaStore.getState>;
prestigeStore: ReturnType<typeof usePrestigeStore.getState>; prestigeStore: ReturnType<typeof usePrestigeStore.getState>;
uiStore: ReturnType<typeof useUIStore.getState>; uiStore: ReturnType<typeof useUIStore.getState>;
combatStore: ReturnType<typeof useCombatStore.getState>; combatStore: ReturnType<typeof useCombatStore.getState>;
// Computed effects from upgrades // Computed effects from upgrades
upgradeEffects: ReturnType<typeof computeEffects>; upgradeEffects: ReturnType<typeof computeEffects>;
// Derived stats // Derived stats
maxMana: number; maxMana: number;
baseRegen: number; baseRegen: number;
@@ -134,25 +105,25 @@ export interface GameContextValue {
incursionStrength: number; incursionStrength: number;
studySpeedMult: number; studySpeedMult: number;
studyCostMult: number; studyCostMult: number;
// Effective regen calculations // Effective regen calculations
effectiveRegenWithSpecials: number; effectiveRegenWithSpecials: number;
manaCascadeBonus: number; manaCascadeBonus: number;
manaWaterfallBonus: number; manaWaterfallBonus: number;
effectiveRegen: number; effectiveRegen: number;
// Has special flags // Has special flags
hasManaWaterfall: boolean; hasManaWaterfall: boolean;
hasFlowSurge: boolean; hasFlowSurge: boolean;
hasManaOverflow: boolean; hasManaOverflow: boolean;
hasEternalFlow: boolean; hasEternalFlow: boolean;
// DPS calculation // DPS calculation
dps: number; dps: number;
// Boons // Boons
activeBoons: ReturnType<typeof getBoonBonuses>; activeBoons: ReturnType<typeof getBoonBonuses>;
// Helpers // Helpers
canCastSpell: (spellId: string) => boolean; canCastSpell: (spellId: string) => boolean;
hasSpecial: (effects: ReturnType<typeof computeEffects>, specialId: string) => boolean; hasSpecial: (effects: ReturnType<typeof computeEffects>, specialId: string) => boolean;
+13 -60
View File
@@ -1,13 +1,11 @@
'use client'; 'use client';
import { canAffordSpellCost, fmt } from '@/lib/game/stores'; import { canAffordSpellCost, fmt } from '@/lib/game/stores';
import { useCombatStore, useSkillStore, useManaStore } from '@/lib/game/stores'; import { useCombatStore, useManaStore } from '@/lib/game/stores';
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants'; import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
import { useStudyStats } from '@/lib/game/hooks/useGameDerived';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
// Format spell cost for display // Format spell cost for display
function formatSpellCost(cost: { type: 'raw' | 'element'; element?: string; amount: number }): string { function formatSpellCost(cost: { type: 'raw' | 'element'; element?: string; amount: number }): string {
@@ -26,33 +24,24 @@ function getSpellCostColor(cost: { type: 'raw' | 'element'; element?: string; am
return ELEMENTS[cost.element || '']?.color || '#9CA3AF'; return ELEMENTS[cost.element || '']?.color || '#9CA3AF';
} }
// Format study time
function formatStudyTime(hours: number): string {
if (hours < 1) return `${Math.round(hours * 60)}m`;
return `${hours.toFixed(1)}h`;
}
export function SpellsTab() { export function SpellsTab() {
const spells = useCombatStore((s) => s.spells); const spells = useCombatStore((s) => s.spells);
const activeSpell = useCombatStore((s) => s.activeSpell); const activeSpell = useCombatStore((s) => s.activeSpell);
const setSpell = useCombatStore((s) => s.setSpell); const setSpell = useCombatStore((s) => s.setSpell);
const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
const setCurrentStudyTarget = useSkillStore((s) => s.setCurrentStudyTarget);
const rawMana = useManaStore((s) => s.rawMana); const rawMana = useManaStore((s) => s.rawMana);
const elements = useManaStore((s) => s.elements); const elements = useManaStore((s) => s.elements);
const { studySpeedMult, studyCostMult } = useStudyStats();
const spellTiers = [0, 1, 2, 3, 4]; const spellTiers = [0, 1, 2, 3, 4];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{spellTiers.map(tier => { {spellTiers.map(tier => {
const spellsInTier = Object.entries(SPELLS_DEF).filter(([, def]) => def.tier === tier); const spellsInTier = Object.entries(SPELLS_DEF).filter(([, def]) => def.tier === tier);
if (spellsInTier.length === 0) return null; if (spellsInTier.length === 0) return null;
const tierNames = ['Basic Spells (Raw Mana)', 'Tier 1 - Elemental', 'Tier 2 - Advanced', 'Tier 3 - Master', 'Tier 4 - Legendary']; const tierNames = ['Basic Spells (Raw Mana)', 'Tier 1 - Elemental', 'Tier 2 - Advanced', 'Tier 3 - Master', 'Tier 4 - Legendary'];
const tierColors = ['text-gray-400', 'text-green-400', 'text-blue-400', 'text-purple-400', 'text-amber-400']; const tierColors = ['text-gray-400', 'text-green-400', 'text-blue-400', 'text-purple-400', 'text-amber-400'];
return ( return (
<div key={tier}> <div key={tier}>
<h3 className={`text-lg font-semibold mb-3 ${tierColors[tier]}`}>{tierNames[tier]}</h3> <h3 className={`text-lg font-semibold mb-3 ${tierColors[tier]}`}>{tierNames[tier]}</h3>
@@ -60,23 +49,14 @@ export function SpellsTab() {
{spellsInTier.map(([id, def]) => { {spellsInTier.map(([id, def]) => {
const state = spells?.[id]; const state = spells?.[id];
const learned = state?.learned; const learned = state?.learned;
const isStudying = currentStudyTarget?.id === id;
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem]; const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
const baseStudyTime = def.studyTime || (def.tier * 4);
const isActive = activeSpell === id; const isActive = activeSpell === id;
const canCast = learned && canAffordSpellCost(def.cost, rawMana, elements); const canCast = learned && canAffordSpellCost(def.cost, rawMana, elements);
// Apply skill modifiers
const studyTime = baseStudyTime / studySpeedMult;
const unlockCost = Math.floor(def.unlock * studyCostMult);
// Can start studying?
const canStudy = !learned && !isStudying && rawMana >= unlockCost;
return ( return (
<Card <Card
key={id} key={id}
className={`bg-gray-900/80 border-gray-700 ${learned ? '' : 'opacity-75'} ${isStudying ? 'border-purple-500' : ''} ${canCast ? 'ring-1 ring-green-500/30' : ''}`} className={`bg-gray-900/80 border-gray-700 ${learned ? '' : 'opacity-75'} ${canCast ? 'ring-1 ring-green-500/30' : ''}`}
> >
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -95,16 +75,16 @@ export function SpellsTab() {
{def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>} {def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>}
<span className="mr-2"> {def.dmg} dmg</span> <span className="mr-2"> {def.dmg} dmg</span>
</div> </div>
{/* Cost display */} {/* Cost display */}
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}> <div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
Cost: {formatSpellCost(def.cost)} Cost: {formatSpellCost(def.cost)}
</div> </div>
{def.desc && ( {def.desc && (
<div className="text-xs text-gray-500 italic">{def.desc}</div> <div className="text-xs text-gray-500 italic">{def.desc}</div>
)} )}
{def.effects && Array.isArray(def.effects) && def.effects.length > 0 && ( {def.effects && Array.isArray(def.effects) && def.effects.length > 0 && (
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
{def.effects.map((eff, i) => ( {def.effects.map((eff, i) => (
@@ -117,7 +97,7 @@ export function SpellsTab() {
))} ))}
</div> </div>
)} )}
{learned ? ( {learned ? (
<div className="flex gap-2"> <div className="flex gap-2">
<Badge className="bg-green-900/50 text-green-300">Learned</Badge> <Badge className="bg-green-900/50 text-green-300">Learned</Badge>
@@ -128,36 +108,9 @@ export function SpellsTab() {
</Button> </Button>
)} )}
</div> </div>
) : isStudying ? (
<div className="space-y-1">
<Progress
value={Math.min(100, ((state?.studyProgress || 0) / studyTime) * 100)}
className="h-2 bg-gray-800"
/>
<div className="text-xs text-purple-400">
Studying... {formatStudyTime(state?.studyProgress || 0)}/{formatStudyTime(studyTime)}
</div>
</div>
) : ( ) : (
<div className="space-y-2"> <div className="text-xs text-gray-500">
<div className="text-xs text-gray-500"> Not yet learned
<span className={studySpeedMult > 1 ? 'text-green-400' : ''}>
Study: {formatStudyTime(studyTime)}{studySpeedMult > 1 && <span className="text-xs ml-1">({Math.round(studySpeedMult * 100)}% speed)</span>}
</span>
{' • '}
<span className={studyCostMult < 1 ? 'text-green-400' : ''}>
Cost: {fmt(unlockCost)} mana{studyCostMult < 1 && <span className="text-xs ml-1">({Math.round(studyCostMult * 100)}% cost)</span>}
</span>
</div>
<Button
size="sm"
variant={canStudy ? 'default' : 'outline'}
disabled={!canStudy}
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
onClick={() => setCurrentStudyTarget({ type: 'spell', id, progress: 0, required: studyTime })}
>
Start Study ({fmt(unlockCost)} mana)
</Button>
</div> </div>
)} )}
</CardContent> </CardContent>
+4 -41
View File
@@ -1,59 +1,24 @@
'use client'; 'use client';
import { useSkillStore, usePrestigeStore, fmt, fmtDec } from '@/lib/game/stores'; import { usePrestigeStore, fmt, fmtDec } from '@/lib/game/stores';
import { ELEMENTS } from '@/lib/game/constants'; import { ELEMENTS } from '@/lib/game/constants';
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived'; import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
import { ManaStatsSection } from './StatsTab/ManaStatsSection'; import { ManaStatsSection } from './StatsTab/ManaStatsSection';
import { CombatStatsSection } from './StatsTab/CombatStatsSection'; import { CombatStatsSection } from './StatsTab/CombatStatsSection';
import { PactStatusSection } from './StatsTab/PactStatusSection'; import { PactStatusSection } from './StatsTab/PactStatusSection';
import { StudyStatsSection } from './StatsTab/StudyStatsSection'; import { StudyStatsSection } from './StatsTab/StudyStatsSection';
import { ElementStatsSection } from './StatsTab/ElementStatsSection'; import { ElementStatsSection } from './StatsTab/ElementStatsSection';
import { ActiveUpgradesSection } from './StatsTab/ActiveUpgradesSection';
import { LoopStatsSection } from './StatsTab/LoopStatsSection'; import { LoopStatsSection } from './StatsTab/LoopStatsSection';
import type { SkillUpgradeChoice } from '@/lib/game/types';
export function StatsTab() { export function StatsTab() {
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const skills = useSkillStore((s) => s.skills);
const skillTiers = useSkillStore((s) => s.skillTiers);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const manaStats = useManaStats(); const manaStats = useManaStats();
const combatStats = useCombatStats(); const combatStats = useCombatStats();
const studyStats = useStudyStats(); const studyStats = useStudyStats();
// Compute element max // Compute element max (base + prestige only)
const elemMax = (() => { const elemMax = 10 + (prestigeUpgrades.elementalAttune || 0) * 25;
const ea = skillTiers?.elemAttune || 1;
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
const level = skills[tieredSkillId] || skills.elemAttune || 0;
const tierMult = getTierMultiplier(tieredSkillId);
return 10 + level * 50 * tierMult + (prestigeUpgrades.elementalAttune || 0) * 25;
})();
// Get all selected skill upgrades
const getAllSelectedUpgrades = (): { skillId: string; upgrade: SkillUpgradeChoice }[] => {
const upgrades: { skillId: string; upgrade: SkillUpgradeChoice }[] = [];
for (const [skillId, selectedIds] of Object.entries(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 as any).upgrades?.find((u: any) => u.id === upgradeId);
if (upgrade) {
upgrades.push({ skillId, upgrade });
}
}
}
}
}
return upgrades;
};
const selectedUpgrades = getAllSelectedUpgrades();
return ( return (
<div className="space-y-4"> <div className="space-y-4">
@@ -65,7 +30,6 @@ export function StatsTab() {
meditationMultiplier={manaStats.meditationMultiplier} meditationMultiplier={manaStats.meditationMultiplier}
upgradeEffects={manaStats.upgradeEffects} upgradeEffects={manaStats.upgradeEffects}
elemMax={elemMax} elemMax={elemMax}
selectedUpgrades={selectedUpgrades}
/> />
<CombatStatsSection <CombatStatsSection
activeSpellDef={combatStats.activeSpellDef} activeSpellDef={combatStats.activeSpellDef}
@@ -82,7 +46,6 @@ export function StatsTab() {
<ElementStatsSection <ElementStatsSection
elemMax={elemMax} elemMax={elemMax}
/> />
<ActiveUpgradesSection selectedUpgrades={selectedUpgrades} />
<LoopStatsSection /> <LoopStatsSection />
</div> </div>
); );
@@ -1,71 +0,0 @@
'use client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Star } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { SKILLS_DEF } from '@/lib/game/constants';
import { SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
import type { SkillUpgradeChoice } from '@/lib/game/types';
interface ActiveUpgradesSectionProps {
selectedUpgrades: { skillId: string; upgrade: SkillUpgradeChoice }[];
}
export function ActiveUpgradesSection({ selectedUpgrades }: ActiveUpgradesSectionProps) {
if (selectedUpgrades.length === 0) {
return (
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
<CardHeader className="pb-2">
<CardTitle className="text-[var(--mana-light)] game-panel-title text-xs flex items-center gap-2">
<Star className="w-4 h-4" />
Active Skill Upgrades (0)
</CardTitle>
</CardHeader>
<CardContent>
<div style={{ color: 'var(--text-muted)' }} className="text-sm">No skill upgrades selected yet. Level skills to 5 or 10 to choose upgrades.</div>
</CardContent>
</Card>
);
}
return (
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
<CardHeader className="pb-2">
<CardTitle className="text-[var(--mana-light)] game-panel-title text-xs flex items-center gap-2">
<Star className="w-4 h-4" />
Active Skill Upgrades ({selectedUpgrades.length})
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{selectedUpgrades.map(({ skillId, upgrade }) => (
<div key={upgrade.id} className="p-2 rounded transition-colors" style={{ border: '1px solid var(--mana-light)/30', background: 'var(--mana-light)/10' }}>
<div className="flex items-center justify-between">
<span style={{ color: 'var(--mana-light)' }} className="text-sm font-semibold">{upgrade.name}</span>
<Badge variant="outline" className="text-xs" style={{ color: 'var(--text-muted)', borderColor: 'var(--border-subtle)' }}>
{SKILLS_DEF[skillId]?.name || skillId}
</Badge>
</div>
<div className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{upgrade.desc}</div>
{upgrade.effect.type === 'multiplier' && (
<div className="text-xs mt-1" style={{ color: 'var(--color-success)' }}>
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
</div>
)}
{upgrade.effect.type === 'bonus' && (
<div className="text-xs mt-1" style={{ color: 'var(--mana-water)' }}>
+{upgrade.effect.value} {upgrade.effect.stat}
</div>
)}
{upgrade.effect.type === 'special' && (
<div className="text-xs mt-1" style={{ color: 'var(--mana-crystal)' }}>
{upgrade.effect.specialDesc || 'Special effect active'}
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
);
}
@@ -3,8 +3,6 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Swords } from 'lucide-react'; import { Swords } from 'lucide-react';
import { fmt, fmtDec } from '@/lib/game/stores'; import { fmt, fmtDec } from '@/lib/game/stores';
import { useSkillStore } from '@/lib/game/stores';
import { getUnifiedEffects } from '@/lib/game/effects';
interface CombatStatsSectionProps { interface CombatStatsSectionProps {
activeSpellDef: any; activeSpellDef: any;
@@ -12,17 +10,6 @@ interface CombatStatsSectionProps {
} }
export function CombatStatsSection({ activeSpellDef, pactMultiplier }: CombatStatsSectionProps) { export function CombatStatsSection({ activeSpellDef, pactMultiplier }: CombatStatsSectionProps) {
const skills = useSkillStore((s) => s.skills);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const skillTiers = useSkillStore((s) => s.skillTiers);
const upgradeEffects = getUnifiedEffects({
skillUpgrades,
skillTiers,
equippedInstances: {},
equipmentInstances: {},
});
return ( return (
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]"> <Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
@@ -38,36 +25,6 @@ export function CombatStatsSection({ activeSpellDef, pactMultiplier }: CombatSta
<span style={{ color: 'var(--text-muted)' }}>Active Spell Base Damage:</span> <span style={{ color: 'var(--text-muted)' }}>Active Spell Base Damage:</span>
<span style={{ color: 'var(--text-secondary)' }}>{activeSpellDef?.dmg || 5}</span> <span style={{ color: 'var(--text-secondary)' }}>{activeSpellDef?.dmg || 5}</span>
</div> </div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Combat Training Bonus:</span>
<span style={{ color: 'var(--mana-fire)' }}>+{(skills.combatTrain || 0) * 5}</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Arcane Fury Multiplier:</span>
<span style={{ color: 'var(--mana-fire)' }}>×{fmtDec(1 + (skills.arcaneFury || 0) * 0.1, 2)}</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Elemental Mastery:</span>
<span style={{ color: 'var(--mana-fire)' }}>×{fmtDec(1 + (skills.elementalMastery || 0) * 0.15, 2)}</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Guardian Bane:</span>
<span style={{ color: 'var(--mana-fire)' }}>×{fmtDec(1 + (skills.guardianBane || 0) * 0.2, 2)} (vs guardians)</span>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Critical Hit Chance:</span>
<span style={{ color: 'var(--mana-light)' }}>{((skills.precision || 0) * 5)}%</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Critical Multiplier:</span>
<span style={{ color: 'var(--mana-light)' }}>1.5x</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Spell Echo Chance:</span>
<span style={{ color: 'var(--mana-light)' }}>{((skills.spellEcho || 0) * 10)}%</span>
</div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Pact Multiplier:</span> <span style={{ color: 'var(--text-muted)' }}>Pact Multiplier:</span>
<span style={{ color: 'var(--mana-light)' }}>×{fmtDec(pactMultiplier, 2)}</span> <span style={{ color: 'var(--mana-light)' }}>×{fmtDec(pactMultiplier, 2)}</span>
@@ -77,8 +34,14 @@ export function CombatStatsSection({ activeSpellDef, pactMultiplier }: CombatSta
<span style={{ color: 'var(--mana-fire)' }}>{fmt(activeSpellDef ? activeSpellDef.dmg * pactMultiplier : 0)}</span> <span style={{ color: 'var(--mana-fire)' }}>{fmt(activeSpellDef ? activeSpellDef.dmg * pactMultiplier : 0)}</span>
</div> </div>
</div> </div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Critical Multiplier:</span>
<span style={{ color: 'var(--mana-light)' }}>1.5x</span>
</div>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
); );
} }
@@ -4,28 +4,17 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { FlaskConical } from 'lucide-react'; import { FlaskConical } from 'lucide-react';
import { ELEMENTS } from '@/lib/game/constants'; import { ELEMENTS } from '@/lib/game/constants';
import { getTierMultiplier } from '@/lib/game/skill-evolution';
import { fmt, fmtDec } from '@/lib/game/stores'; import { fmt, fmtDec } from '@/lib/game/stores';
import { useSkillStore, usePrestigeStore, useManaStore } from '@/lib/game/stores'; import { usePrestigeStore, useManaStore } from '@/lib/game/stores';
interface ElementStatsSectionProps { interface ElementStatsSectionProps {
elemMax: number; elemMax: number;
} }
export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) { export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
const skills = useSkillStore((s) => s.skills);
const skillTiers = useSkillStore((s) => s.skillTiers);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const elements = useManaStore((s) => s.elements); const elements = useManaStore((s) => s.elements);
const getElemAttunementBonus = () => {
const ea = skillTiers?.elemAttune || 1;
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
const level = skills[tieredSkillId] || skills.elemAttune || 0;
const tierMult = getTierMultiplier(tieredSkillId);
return level * 50 * tierMult;
};
return ( return (
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]"> <Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
@@ -41,10 +30,6 @@ export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
<span style={{ color: 'var(--text-muted)' }}>Element Capacity:</span> <span style={{ color: 'var(--text-muted)' }}>Element Capacity:</span>
<span style={{ color: 'var(--color-success)' }}>{elemMax}</span> <span style={{ color: 'var(--color-success)' }}>{elemMax}</span>
</div> </div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Elem. Attunement Bonus:</span>
<span style={{ color: 'var(--color-success)' }}>+{getElemAttunementBonus()}</span>
</div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Prestige Attunement:</span> <span style={{ color: 'var(--text-muted)' }}>Prestige Attunement:</span>
<span style={{ color: 'var(--color-success)' }}>+{(prestigeUpgrades.elementalAttune || 0) * 25}</span> <span style={{ color: 'var(--color-success)' }}>+{(prestigeUpgrades.elementalAttune || 0) * 25}</span>
@@ -55,10 +40,6 @@ export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
<span style={{ color: 'var(--text-muted)' }}>Unlocked Elements:</span> <span style={{ color: 'var(--text-muted)' }}>Unlocked Elements:</span>
<span style={{ color: 'var(--color-success)' }}>{Object.values(elements || {}).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span> <span style={{ color: 'var(--color-success)' }}>{Object.values(elements || {}).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
</div> </div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Elem. Crafting Bonus:</span>
<span style={{ color: 'var(--color-success)' }}>×{fmtDec(1 + (skills.elemCrafting || 0) * 0.25, 2)}</span>
</div>
</div> </div>
</div> </div>
<Separator className="bg-[var(--border-subtle)] my-3" /> <Separator className="bg-[var(--border-subtle)] my-3" />
@@ -79,4 +60,4 @@ export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
</CardContent> </CardContent>
</Card> </Card>
); );
} }
@@ -4,21 +4,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { RotateCcw } from 'lucide-react'; import { RotateCcw } from 'lucide-react';
import { fmt } from '@/lib/game/stores'; import { fmt } from '@/lib/game/stores';
import { useCombatStore, usePrestigeStore, useManaStore, useSkillStore } from '@/lib/game/stores'; import { useCombatStore, usePrestigeStore, useManaStore } from '@/lib/game/stores';
export function LoopStatsSection() { export function LoopStatsSection() {
const spells = useCombatStore((s) => s.spells); const spells = useCombatStore((s) => s.spells);
const skills = useSkillStore((s) => s.skills);
const insight = usePrestigeStore((s) => s.insight); const insight = usePrestigeStore((s) => s.insight);
const totalInsight = usePrestigeStore((s) => s.totalInsight); const totalInsight = usePrestigeStore((s) => s.totalInsight);
const maxFloorReached = useCombatStore((s) => s.maxFloorReached); const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
const totalManaGathered = useManaStore((s) => s.totalManaGathered); const totalManaGathered = useManaStore((s) => s.totalManaGathered);
const loopCount = usePrestigeStore((s) => s.loopCount); const loopCount = usePrestigeStore((s) => s.loopCount);
const memorySlots = usePrestigeStore((s) => s.memorySlots); const memorySlots = usePrestigeStore((s) => s.memorySlots);
const spellsLearned = Object.values(spells || {}).filter((s: any) => s.learned).length; const spellsLearned = Object.values(spells || {}).filter((s: any) => s.learned).length;
const totalSkillLevels = Object.values(skills || {}).reduce((a: number, b: number) => a + b, 0);
return ( return (
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]"> <Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
@@ -52,10 +50,6 @@ export function LoopStatsSection() {
<div className="text-xl font-bold text-[var(--text-secondary)] game-mono">{spellsLearned}</div> <div className="text-xl font-bold text-[var(--text-secondary)] game-mono">{spellsLearned}</div>
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>Spells Learned</div> <div className="text-xs" style={{ color: 'var(--text-muted)' }}>Spells Learned</div>
</div> </div>
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
<div className="text-xl font-bold text-[var(--text-secondary)] game-mono">{totalSkillLevels}</div>
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>Total Skill Levels</div>
</div>
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center"> <div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
<div className="text-xl font-bold text-[var(--text-secondary)] game-mono">{fmt(totalManaGathered)}</div> <div className="text-xl font-bold text-[var(--text-secondary)] game-mono">{fmt(totalManaGathered)}</div>
<div className="text-xs" style={{ color: 'var(--text-muted)' }}>Total Mana Gathered</div> <div className="text-xs" style={{ color: 'var(--text-muted)' }}>Total Mana Gathered</div>
@@ -3,8 +3,6 @@
import { fmt, fmtDec } from '@/lib/game/stores'; import { fmt, fmtDec } from '@/lib/game/stores';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Droplet } from 'lucide-react'; import { Droplet } from 'lucide-react';
import type { SkillUpgradeChoice } from '@/lib/game/types';
import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
interface ManaStatsSectionProps { interface ManaStatsSectionProps {
maxMana: number; maxMana: number;
@@ -14,7 +12,6 @@ interface ManaStatsSectionProps {
meditationMultiplier: number; meditationMultiplier: number;
upgradeEffects: any; upgradeEffects: any;
elemMax: number; elemMax: number;
selectedUpgrades: { skillId: string; upgrade: SkillUpgradeChoice }[];
} }
export function ManaStatsSection({ export function ManaStatsSection({
@@ -25,16 +22,7 @@ export function ManaStatsSection({
meditationMultiplier, meditationMultiplier,
upgradeEffects, upgradeEffects,
elemMax, elemMax,
selectedUpgrades,
}: ManaStatsSectionProps) { }: ManaStatsSectionProps) {
const skills = useSkillStore((s) => s.skills);
const skillTiers = useSkillStore((s) => s.skillTiers);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const getTierMultiplier = (skillId: string) => {
return 1;
};
return ( return (
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]"> <Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
@@ -50,22 +38,6 @@ export function ManaStatsSection({
<span style={{ color: 'var(--text-muted)' }}>Base Max Mana:</span> <span style={{ color: 'var(--text-muted)' }}>Base Max Mana:</span>
<span style={{ color: 'var(--text-secondary)' }}>100</span> <span style={{ color: 'var(--text-secondary)' }}>100</span>
</div> </div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Mana Well Bonus:</span>
<span style={{ color: 'var(--mana-water)' }}>
{(() => {
const mw = skillTiers?.manaWell || 1;
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
const level = (skills || {})[tieredSkillId] || (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 style={{ color: 'var(--text-muted)' }}>Prestige Mana Well:</span>
<span style={{ color: 'var(--mana-water)' }}>+{fmt((prestigeUpgrades.manaWell || 0) * 500)}</span>
</div>
{upgradeEffects.maxManaBonus > 0 && ( {upgradeEffects.maxManaBonus > 0 && (
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span style={{ color: 'var(--mana-light)' }}>Upgrade Mana Bonus:</span> <span style={{ color: 'var(--mana-light)' }}>Upgrade Mana Bonus:</span>
@@ -88,30 +60,6 @@ export function ManaStatsSection({
<span style={{ color: 'var(--text-muted)' }}>Base Regen:</span> <span style={{ color: 'var(--text-muted)' }}>Base Regen:</span>
<span style={{ color: 'var(--text-secondary)' }}>2/hr</span> <span style={{ color: 'var(--text-secondary)' }}>2/hr</span>
</div> </div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Mana Flow Bonus:</span>
<span style={{ color: 'var(--mana-water)' }}>
{(() => {
const mf = skillTiers?.manaFlow || 1;
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
const level = skills[tieredSkillId] || skills.manaFlow || 0;
const tierMult = getTierMultiplier(tieredSkillId);
return `+${fmtDec(level * 1 * tierMult, 2)}/hr (${level} lvl × 1 × ${tierMult}x tier)`;
})()}
</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Mana Spring Bonus:</span>
<span style={{ color: 'var(--mana-water)' }}>+{(skills.manaSpring || 0) * 2}/hr</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Prestige Mana Flow:</span>
<span style={{ color: 'var(--mana-water)' }}>+{fmtDec((prestigeUpgrades.manaFlow || 0) * 0.5)}/hr</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Temporal Echo:</span>
<span style={{ color: 'var(--mana-water)' }}>×{fmtDec(1 + (prestigeUpgrades.temporalEcho || 0) * 0.1, 2)}</span>
</div>
<div className="flex justify-between text-sm font-semibold border-t border-[var(--border-subtle)] pt-2"> <div className="flex justify-between text-sm font-semibold border-t border-[var(--border-subtle)] pt-2">
<span style={{ color: 'var(--text-secondary)' }}>Base Regen:</span> <span style={{ color: 'var(--text-secondary)' }}>Base Regen:</span>
<span style={{ color: 'var(--mana-water)' }}>{fmtDec(baseRegen, 2)}/hr</span> <span style={{ color: 'var(--mana-water)' }}>{fmtDec(baseRegen, 2)}/hr</span>
@@ -142,18 +90,6 @@ export function ManaStatsSection({
<span style={{ color: 'var(--text-muted)' }}>Click Mana Value:</span> <span style={{ color: 'var(--text-muted)' }}>Click Mana Value:</span>
<span style={{ color: 'var(--mana-crystal)' }}>+{clickMana}</span> <span style={{ color: 'var(--mana-crystal)' }}>+{clickMana}</span>
</div> </div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Mana Tap Bonus:</span>
<span style={{ color: 'var(--mana-crystal)' }}>+{skills.manaTap || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Mana Surge Bonus:</span>
<span style={{ color: 'var(--mana-crystal)' }}>+{(skills.manaSurge || 0) * 3}</span>
</div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Mana Overflow:</span>
<span style={{ color: 'var(--mana-crystal)' }}>×{fmtDec(1 + (skills.manaOverflow || 0) * 0.25, 2)}</span>
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
@@ -173,7 +109,7 @@ export function ManaStatsSection({
</div> </div>
</div> </div>
{/* Special Effects */} {/* Special Effects */}
{(upgradeEffects.hasSteadyStream || upgradeEffects.hasManaTorrent || {(upgradeEffects.hasSteadyStream || upgradeEffects.hasManaTorrent ||
upgradeEffects.hasDesperateWells || upgradeEffects.manaCascadeBonus > 0 || upgradeEffects.hasDesperateWells || upgradeEffects.manaCascadeBonus > 0 ||
upgradeEffects.manaWaterfallBonus > 0) && ( upgradeEffects.manaWaterfallBonus > 0) && (
<> <>
@@ -240,4 +176,4 @@ export function ManaStatsSection({
</CardContent> </CardContent>
</Card> </Card>
); );
} }
@@ -3,7 +3,6 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BookOpen } from 'lucide-react'; import { BookOpen } from 'lucide-react';
import { fmtDec } from '@/lib/game/stores'; import { fmtDec } from '@/lib/game/stores';
import { useSkillStore } from '@/lib/game/stores';
interface StudyStatsSectionProps { interface StudyStatsSectionProps {
studySpeedMult: number; studySpeedMult: number;
@@ -11,8 +10,6 @@ interface StudyStatsSectionProps {
} }
export function StudyStatsSection({ studySpeedMult, studyCostMult }: StudyStatsSectionProps) { export function StudyStatsSection({ studySpeedMult, studyCostMult }: StudyStatsSectionProps) {
const skills = useSkillStore((s) => s.skills);
return ( return (
<Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]"> <Card className="bg-[var(--bg-panel)] border-[var(--border-subtle)]">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
@@ -28,29 +25,21 @@ export function StudyStatsSection({ studySpeedMult, studyCostMult }: StudyStatsS
<span style={{ color: 'var(--text-muted)' }}>Study Speed:</span> <span style={{ color: 'var(--text-muted)' }}>Study Speed:</span>
<span style={{ color: 'var(--mana-crystal)' }}>×{fmtDec(studySpeedMult, 2)}</span> <span style={{ color: 'var(--mana-crystal)' }}>×{fmtDec(studySpeedMult, 2)}</span>
</div> </div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Quick Learner Bonus:</span>
<span style={{ color: 'var(--mana-crystal)' }}>+{((skills.quickLearner || 0) * 10)}%</span>
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Study Cost:</span> <span style={{ color: 'var(--text-muted)' }}>Study Cost:</span>
<span style={{ color: 'var(--mana-crystal)' }}>{Math.round(studyCostMult * 100)}%</span> <span style={{ color: 'var(--mana-crystal)' }}>{Math.round(studyCostMult * 100)}%</span>
</div> </div>
<div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Focused Mind Bonus:</span>
<span style={{ color: 'var(--mana-crystal)' }}>-{((skills.focusedMind || 0) * 5)}%</span>
</div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span style={{ color: 'var(--text-muted)' }}>Progress Retention:</span> <span style={{ color: 'var(--text-muted)' }}>Progress Retention:</span>
<span style={{ color: 'var(--mana-crystal)' }}>{Math.round((1 + (skills.knowledgeRetention || 0) * 0.2) * 100)}%</span> <span style={{ color: 'var(--mana-crystal)' }}>100%</span>
</div> </div>
</div> </div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
); );
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { BookOpen, X } from 'lucide-react'; import { BookOpen, X } from 'lucide-react';
import { SKILLS_DEF, SPELLS_DEF } from '@/lib/game/constants'; import { SKILLS_DEF, SPELLS_DEF } from '@/lib/game/constants';
import { formatStudyTime } from '@/lib/game/formatting'; import { formatStudyTime } from '@/lib/game/utils/formatting';
import type { StudyTarget } from '@/lib/game/types'; import type { StudyTarget } from '@/lib/game/types';
interface StudyProgressProps { interface StudyProgressProps {
+1 -1
View File
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { fmt } from '@/lib/game/stores'; import { fmt } from '@/lib/game/stores';
import { formatHour } from '@/lib/game/formatting'; import { formatHour } from '@/lib/game/utils/formatting';
interface TimeDisplayProps { interface TimeDisplayProps {
day: number; day: number;
@@ -23,7 +23,6 @@ import {
removeEffectFromDesign, removeEffectFromDesign,
} from './EnchantmentDesigner/utils'; } from './EnchantmentDesigner/utils';
import { useCraftingStore } from '@/lib/game/stores'; import { useCraftingStore } from '@/lib/game/stores';
import { useSkillStore } from '@/lib/game/stores';
export function EnchantmentDesigner({ export function EnchantmentDesigner({
selectedEquipmentType, selectedEquipmentType,
@@ -44,15 +43,8 @@ export function EnchantmentDesigner({
const unlockedEffects = useCraftingStore((s) => s.unlockedEffects); const unlockedEffects = useCraftingStore((s) => s.unlockedEffects);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
// Skill store selectors
const skills = useSkillStore((s) => s.skills);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const enchantingLevel = skills?.enchanting || 0;
const efficiencyBonus = (skillUpgrades?.['efficientEnchant'] || []).length * 0.05 || 0;
// Calculate total capacity cost for current design // Calculate total capacity cost for current design
const designCapacityCost = calculateDesignCapacityCost(selectedEffects, efficiencyBonus); const designCapacityCost = calculateDesignCapacityCost(selectedEffects, 0);
// Get capacity limit for selected equipment type // Get capacity limit for selected equipment type
const selectedEquipmentCapacity = getEquipmentCapacity(selectedEquipmentType); const selectedEquipmentCapacity = getEquipmentCapacity(selectedEquipmentType);
@@ -62,7 +54,7 @@ export function EnchantmentDesigner({
// Add effect to design // Add effect to design
const addEffect = (effectId: string) => { const addEffect = (effectId: string) => {
addEffectToDesign(effectId, selectedEffects, efficiencyBonus, setSelectedEffects); addEffectToDesign(effectId, selectedEffects, 0, setSelectedEffects);
}; };
// Remove effect from design // Remove effect from design
@@ -117,8 +109,8 @@ export function EnchantmentDesigner({
setSelectedEffects={setSelectedEffects} setSelectedEffects={setSelectedEffects}
availableEffects={availableEffects} availableEffects={availableEffects}
incompatibleEffects={incompatibleEffects} incompatibleEffects={incompatibleEffects}
enchantingLevel={enchantingLevel} enchantingLevel={0}
efficiencyBonus={efficiencyBonus} efficiencyBonus={0}
designProgress={designProgress} designProgress={designProgress}
addEffect={addEffect} addEffect={addEffect}
removeEffect={removeEffect} removeEffect={removeEffect}
@@ -14,7 +14,7 @@ import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types'; import type { EquipmentInstance, AppliedEnchantment, LootInventory, EquipmentCraftingProgress } from '@/lib/game/types';
import type { EquipmentSlot } from '@/lib/game/types'; import type { EquipmentSlot } from '@/lib/game/types';
import { fmt } from '@/lib/game/stores'; import { fmt } from '@/lib/game/stores';
import { useGameStore, useCraftingStore, useManaStore, useSkillStore } from '@/lib/game/stores'; import { useGameStore, useCraftingStore, useManaStore } from '@/lib/game/stores';
import { useGameToast } from '@/components/game/GameToast'; import { useGameToast } from '@/components/game/GameToast';
export interface EnchantmentPreparerProps { export interface EnchantmentPreparerProps {
@@ -31,7 +31,6 @@ export function EnchantmentPreparer({
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const preparationProgress = useCraftingStore((s) => s.preparationProgress); const preparationProgress = useCraftingStore((s) => s.preparationProgress);
const rawMana = useManaStore((s) => s.rawMana); const rawMana = useManaStore((s) => s.rawMana);
const skills = useSkillStore((s) => s.skills);
const startPreparing = useCraftingStore((s) => s.startPreparing); const startPreparing = useCraftingStore((s) => s.startPreparing);
const cancelPreparation = useCraftingStore((s) => s.cancelPreparation); const cancelPreparation = useCraftingStore((s) => s.cancelPreparation);
+3 -5
View File
@@ -2,11 +2,9 @@
// Re-exports all game tab components for cleaner imports // Re-exports all game tab components for cleaner imports
// Tab components // Tab components
export { CraftingTab } from './tabs/CraftingTab'; export { CraftingTab } from './crafting';
export { SpireTab } from './tabs/SpireTab'; export { SpellsTab } from './SpellsTab';
export { SpellsTab } from './tabs/SpellsTab'; export { StatsTab } from './StatsTab';
export { SkillsTab } from './SkillsTab';
export { StatsTab } from './tabs/StatsTab';
// UI components // UI components
export { ActionButtons } from './ActionButtons'; export { ActionButtons } from './ActionButtons';
-45
View File
@@ -1,45 +0,0 @@
'use client';
import { fmt } from '@/lib/game/stores';
import { formatHour } from '@/lib/game/formatting';
import { TimeDisplay } from '@/components/game/TimeDisplay';
interface HeaderProps {
day: number;
hour: number;
insight: number;
}
export function Header({ day, hour, insight }: HeaderProps) {
return (
<header className="sticky top-0 z-50 bg-[var(--bg-surface)]/95 backdrop-blur-sm border-b border-[var(--border-subtle)] px-4 py-2">
<div className="flex items-center justify-between">
{/* Game Title - always visible */}
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
{/* Desktop header content */}
<div className="hidden md:flex items-center gap-4">
<TimeDisplay
day={day}
hour={hour}
insight={insight}
/>
</div>
{/* Mobile header content - compact */}
<div className="flex md:hidden items-center gap-2">
<div className="text-center">
<div className="text-sm font-bold game-mono text-[var(--mana-light)]">
D{day} {formatHour(hour)}
</div>
<div className="text-xs text-[var(--text-secondary)]">
{fmt(insight)} 💎
</div>
</div>
</div>
</div>
</header>
);
}
Header.displayName = "Header";
-142
View File
@@ -1,142 +0,0 @@
'use client';
import { useState } from 'react';
import { TabsTrigger } from '@/components/ui/tabs';
import { Separator } from '@/components/ui/separator';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
Mountain,
Sparkles,
Brain,
Wand2,
Bone,
Shield,
Hammer,
Gem,
Trophy,
FlaskConical,
BarChart3,
BookOpen,
Wrench
} from 'lucide-react';
interface TabBarProps {
activeTab: string;
onTabChange: (value: string) => void;
isMobile?: boolean;
}
// Tab configuration with groups
const TAB_GROUPS = [
{
name: 'World',
tabs: [
{ value: 'spire', label: 'Spire', icon: Mountain, mobileLabel: 'Spire' },
{ value: 'attunements', label: 'Attune', icon: Sparkles, mobileLabel: 'Attune' },
]
},
{
name: 'Power',
tabs: [
{ value: 'skills', label: 'Skills', icon: Brain, mobileLabel: 'Skills' },
{ value: 'spells', label: 'Spells', icon: Wand2, mobileLabel: 'Spells' },
{ value: 'golemancy', label: 'Golems', icon: Bone, mobileLabel: 'Golems' },
]
},
{
name: 'Gear',
tabs: [
{ value: 'equipment', label: 'Gear', icon: Shield, mobileLabel: 'Gear' },
{ value: 'crafting', label: 'Craft', icon: Hammer, mobileLabel: 'Craft' },
{ value: 'loot', label: 'Loot', icon: Gem, mobileLabel: 'Loot' },
]
},
{
name: 'Meta',
tabs: [
{ value: 'achievements', label: 'Achieve', icon: Trophy, mobileLabel: 'Achieve' },
{ value: 'stats', label: 'Stats', icon: BarChart3, mobileLabel: 'Stats' },
{ value: 'debug', label: 'Debug', icon: Wrench, mobileLabel: 'Debug' },
]
}
];
export function TabBar({ activeTab, onTabChange, isMobile = false }: TabBarProps) {
if (isMobile) {
return (
<TooltipProvider>
<div className="flex overflow-x-auto scrollbar-thin gap-1 pb-2" style={{ flexWrap: 'nowrap' }}>
{TAB_GROUPS.map((group, groupIndex) => (
<div key={group.name} className="flex items-center flex-shrink-0">
{groupIndex > 0 && (
<Separator orientation="vertical" className="h-6 mx-1 bg-[var(--border-subtle)]" />
)}
{group.tabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.value;
return (
<Tooltip key={tab.value}>
<TooltipTrigger asChild>
<button
onClick={() => onTabChange(tab.value)}
className={`
flex items-center justify-center p-2 flex-shrink-0 transition-all border text-[var(--font-display)]
${isActive
? 'border-[var(--border-accent)] bg-[var(--bg-raised)] text-[var(--interactive-primary)]'
: 'border-transparent text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-panel)] hover:border-[var(--border-subtle)]'
}
`}
aria-label={tab.label}
>
<Icon className="w-5 h-5" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>{tab.label}</p>
</TooltipContent>
</Tooltip>
);
})}
</div>
))}
</div>
</TooltipProvider>
);
}
// Desktop view - grouped tabs with separators
return (
<div className="flex items-center gap-1 w-full" style={{ flexWrap: 'nowrap' }}>
{TAB_GROUPS.map((group, groupIndex) => (
<div key={group.name} className="flex items-center flex-shrink-0">
{groupIndex > 0 && (
<Separator orientation="vertical" className="h-6 mx-2 bg-[var(--border-subtle)]" />
)}
{group.tabs.map((tab) => {
const isActive = activeTab === tab.value;
return (
<TabsTrigger
key={tab.value}
value={tab.value}
className={`
text-xs px-3 py-1.5 relative transition-all whitespace-nowrap text-[var(--font-display)] tracking-wider
${isActive
? 'text-[var(--interactive-primary)]'
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
}
`}
style={isActive ? {
borderBottom: '2px solid var(--border-accent)',
} : {}}
>
{tab.label}
</TabsTrigger>
);
})}
</div>
))}
</div>
);
}
TabBar.displayName = "TabBar";
@@ -1,62 +0,0 @@
'use client';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { BookOpen, X } from 'lucide-react';
import { useGameContext } from '../GameContext';
import { formatStudyTime } from '../types';
import { SKILLS_DEF, SPELLS_DEF } from '@/lib/game/constants';
interface StudyProgressProps {
target: NonNullable<ReturnType<typeof useGameContext>['store']['currentStudyTarget']>;
showCancel?: boolean;
speedLabel?: string;
}
export function StudyProgress({ target, showCancel = true, speedLabel }: StudyProgressProps) {
const { store, studySpeedMult } = useGameContext();
const progressPct = Math.min(100, (target.progress / target.required) * 100);
const isSkill = target.type === 'skill';
const def = isSkill ? SKILLS_DEF[target.id] : SPELLS_DEF[target.id];
const currentLevel = isSkill ? store.skills[target.id] || 0 : 0;
const handleCancel = () => {
// Calculate retention bonus from knowledge retention skill
const retentionBonus = 0.2 * (store.skills.knowledgeRetention || 0);
store.cancelStudy(retentionBonus);
};
return (
<div className="p-3 rounded border border-purple-600/50 bg-purple-900/20">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<BookOpen className="w-4 h-4 text-purple-400" />
<span className="text-sm font-semibold text-purple-300">
{def?.name}
{isSkill && ` Lv.${currentLevel + 1}`}
</span>
</div>
{showCancel && (
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
onClick={handleCancel}
>
<X className="w-4 h-4" />
</Button>
)}
</div>
<Progress value={progressPct} className="h-2 bg-gray-800" />
<div className="flex justify-between text-xs text-gray-400 mt-1">
<span>
{formatStudyTime(target.progress)} / {formatStudyTime(target.required)}
</span>
<span>{speedLabel ?? `${studySpeedMult.toFixed(1)}x speed`}</span>
</div>
</div>
);
}
StudyProgress.displayName = "StudyProgress";
@@ -1,128 +0,0 @@
'use client';
import { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useGameContext } from '../GameContext';
import { SKILLS_DEF } from '@/lib/game/constants';
interface UpgradeDialogProps {
skillId: string | null;
milestone: 5 | 10;
onClose: () => void;
}
export function UpgradeDialog({ skillId, milestone, onClose }: UpgradeDialogProps) {
const { store } = useGameContext();
const skillDef = skillId ? SKILLS_DEF[skillId] : null;
const { available, selected: alreadySelected } = skillId
? store.getSkillUpgradeChoices(skillId, milestone)
: { available: [], selected: [] };
// Use local state for selections within this dialog session
const [pendingSelections, setPendingSelections] = useState<string[]>(() => [...alreadySelected]);
const toggleUpgrade = (upgradeId: string) => {
setPendingSelections((prev) => {
if (prev.includes(upgradeId)) {
return prev.filter((id) => id !== upgradeId);
} else if (prev.length < 2) {
return [...prev, upgradeId];
}
return prev;
});
};
const handleDone = () => {
if (pendingSelections.length === 2 && skillId) {
store.commitSkillUpgrades(skillId, pendingSelections);
}
onClose();
};
const handleOpenChange = (open: boolean) => {
if (!open) {
setPendingSelections([...alreadySelected]);
onClose();
}
};
// Don't render if no skill selected
if (!skillId) return null;
return (
<Dialog open={!!skillId} onOpenChange={handleOpenChange}>
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
<DialogHeader>
<DialogTitle className="text-amber-400">Choose Upgrade - {skillDef?.name || skillId}</DialogTitle>
<DialogDescription className="text-gray-400">
Level {milestone} Milestone - Select 2 upgrades ({pendingSelections.length}/2 chosen)
</DialogDescription>
</DialogHeader>
<div className="space-y-2 mt-4">
{available.map((upgrade) => {
const isSelected = pendingSelections.includes(upgrade.id);
const canToggle = pendingSelections.length < 2 || isSelected;
return (
<div
key={upgrade.id}
className={`p-3 rounded border cursor-pointer transition-all ${
isSelected
? 'border-amber-500 bg-amber-900/30'
: canToggle
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
}`}
onClick={() => {
if (canToggle) {
toggleUpgrade(upgrade.id);
}
}}
>
<div className="flex items-center justify-between">
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
</div>
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
{upgrade.effect.type === 'multiplier' && (
<div className="text-xs text-green-400 mt-1">
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
</div>
)}
{upgrade.effect.type === 'bonus' && (
<div className="text-xs text-blue-400 mt-1">
+{upgrade.effect.value} {upgrade.effect.stat}
</div>
)}
{upgrade.effect.type === 'special' && (
<div className="text-xs text-cyan-400 mt-1"> {upgrade.desc || 'Special effect'}</div>
)}
</div>
);
})}
</div>
<div className="flex justify-end gap-2 mt-4">
<Button
variant="outline"
onClick={() => {
setPendingSelections([...alreadySelected]);
onClose();
}}
>
Cancel
</Button>
<Button variant="default" onClick={handleDone} disabled={pendingSelections.length !== 2}>
{pendingSelections.length < 2 ? `Select ${2 - pendingSelections.length} more` : 'Confirm'}
</Button>
</div>
</DialogContent>
</Dialog>
);
}
UpgradeDialog.displayName = "UpgradeDialog";
@@ -1,67 +0,0 @@
'use client';
import { fmtDec } from '@/lib/game/stores';
import { GUARDIANS } from '@/lib/game/constants';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Swords } from 'lucide-react';
// Modular stores
import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
export function CombatStatsSection() {
// Get state from modular stores
const skills = useSkillStore((s) => s.skills);
const signedPacts = usePrestigeStore((s) => s.signedPacts);
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-red-400 text-sm 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">+{(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 + (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 + (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 + (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">{(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">{(skills.spellEcho || 0) * 10}%</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-amber-300">×{fmtDec(signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}</span>
</div>
</div>
</div>
</CardContent>
</Card>
);
}
CombatStatsSection.displayName = "CombatStatsSection";
@@ -1,268 +0,0 @@
'use client';
import { getTierMultiplier } from '@/lib/game/skill-evolution';
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
import { fmt, fmtDec } from '@/lib/game/stores';
import type { UnifiedEffects } from '@/lib/game/effects';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Droplet } from 'lucide-react';
// Modular stores
import { useSkillStore, usePrestigeStore, useManaStore } from '@/lib/game/stores';
export interface ManaStatsSectionProps {
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({
upgradeEffects,
maxMana,
baseRegen,
clickMana,
meditationMultiplier,
effectiveRegen,
incursionStrength,
manaCascadeBonus,
manaWaterfallBonus,
hasManaWaterfall,
hasFlowSurge,
hasManaOverflow,
hasEternalFlow,
}: ManaStatsSectionProps) {
// Get state from modular stores
const skills = useSkillStore((s) => s.skills);
const skillTiers = useSkillStore((s) => s.skillTiers);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const rawMana = useManaStore((s) => s.rawMana);
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 = skillTiers?.manaWell || 1;
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
const level = skills[tieredSkillId] || 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((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 = skillTiers?.manaFlow || 1;
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
const level = skills[tieredSkillId] || 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">+{(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((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 + (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">+{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">+{(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 + (skills.manaOverflow || 0) * 0.25, 2)}</span>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className={`font-semibold ${meditationMultiplier > 1.5 ? 'text-purple-400' : 'text-gray-300'}`}>
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-300">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) && 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) && 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.displayName = "ManaStatsSection";
@@ -1,239 +0,0 @@
'use client';
import { ELEMENTS } from '@/lib/game/constants';
import { fmt, fmtDec } from '@/lib/game/stores';
import { ATTUNEMENTS_DEF, getAttunementConversionRate } from '@/lib/game/data/attunements';
import { computeMaxMana, computeElementMax } from '@/lib/game/stores';
import { computeEffectiveRegenForDisplay } from '@/lib/game/store-modules/computed-stats';
import { getUnifiedEffects } from '@/lib/game/effects';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Droplet } from 'lucide-react';
import { Separator } from '@/components/ui/separator';
// Modular stores
import { useManaStore, useSkillStore, usePrestigeStore } from '@/lib/game/stores';
export function ManaTypeBreakdown() {
// Get state from modular stores
const skills = useSkillStore((s) => s.skills);
const skillTiers = useSkillStore((s) => s.skillTiers);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const elements = useManaStore((s) => s.elements);
const rawMana = useManaStore((s) => s.rawMana);
// attunements is not in modular stores - using empty object as fallback
const attunements: Record<string, { active: boolean; level: number; experience: number }> = {};
// Compute unified effects for regen calculations
const effects = getUnifiedEffects({
skillUpgrades,
skillTiers,
equippedInstances: {},
equipmentInstances: {}
});
// Get effective regen info for raw mana
const regenInfo = computeEffectiveRegenForDisplay({
skills,
prestigeUpgrades,
skillUpgrades,
skillTiers,
elements,
rawMana,
attunements
} as any, effects);
// Compute max mana
const maxMana = computeMaxMana({
skills,
prestigeUpgrades,
skillUpgrades,
skillTiers
}, effects);
// Get unlocked elements sorted by category then name
const unlockedElements = Object.entries(elements)
.filter(([, state]) => state.unlocked)
.map(([id, state]) => {
const def = ELEMENTS[id];
if (!def) return null;
const elemMax = computeElementMax({
skills,
prestigeUpgrades,
skillUpgrades,
skillTiers
} as any, effects, id);
return {
id,
name: def.name,
sym: def.sym,
color: def.color,
current: state.current,
max: elemMax,
cat: def.cat,
recipe: def.recipe,
};
})
.filter(Boolean)
.sort((a, b) => {
if (!a || !b) return 0;
if (a.cat !== b.cat) return a.cat.localeCompare(b.cat);
return a.name.localeCompare(b.name);
});
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">
<Droplet className="w-4 h-4" />
Mana Type Breakdown
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{/* Raw Mana Section */}
<div className="p-3 bg-gray-800/50 rounded border border-gray-700">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-lg">🌀</span>
<span className="font-semibold text-purple-300">Raw Mana</span>
<span className="text-xs text-gray-500">(base)</span>
</div>
<div className="text-sm">
<span className="text-gray-300">{fmt(rawMana)}</span>
<span className="text-gray-500"> / </span>
<span className="text-gray-400">{fmt(maxMana)}</span>
</div>
</div>
{/* Progress bar */}
<div className="w-full bg-gray-700 rounded-full h-2 mb-3">
<div
className="h-2 rounded-full transition-all duration-300 bg-purple-500"
style={{ width: `${Math.min(100, (rawMana / maxMana) * 100)}%` }}
/>
</div>
{/* Regen info */}
<div className="text-xs space-y-1">
<div className="flex justify-between">
<span className="text-gray-400">Base Regen:</span>
<span className="text-green-400">{fmtDec(regenInfo.rawRegen, 2)}/hr</span>
</div>
{regenInfo.conversionDrain > 0 && (
<div className="flex justify-between">
<span className="text-gray-400">Conversion Drain:</span>
<span className="text-red-400">-{fmtDec(regenInfo.conversionDrain, 2)}/hr</span>
</div>
)}
<Separator className="bg-gray-700 my-1" />
<div className="flex justify-between font-semibold">
<span className="text-gray-300">Effective Regen:</span>
<span className="text-green-400">{fmtDec(regenInfo.effectiveRegen, 2)}/hr</span>
</div>
{/* Show conversion drains by attunement */}
{attunements && Object.keys(attunements).length > 0 && (
<>
<Separator className="bg-gray-700 my-1" />
<div className="text-gray-400 mb-1">Conversion Drains:</div>
{Object.entries(attunements).map(([attId, attState]) => {
const attDef = ATTUNEMENTS_DEF[attId];
if (!attDef || attState.level === 0) return null;
const rate = getAttunementConversionRate(attId, attState.level);
if (rate <= 0) return null;
return (
<div key={attId} className="flex justify-between pl-2">
<span className="text-gray-500">{attDef.name}:</span>
<span className="text-red-400">-{fmtDec(rate, 2)}/hr</span>
</div>
);
})}
</>
)}
</div>
</div>
<Separator className="bg-gray-700" />
{/* Elemental Mana Sections */}
{unlockedElements.map((elem) => {
if (!elem) return null;
// Find attunements that convert TO this element
const convertingAttunements = Object.entries(attunements || {})
.filter(([attId, attState]) => {
if (!attState.active) return false;
const attDef = ATTUNEMENTS_DEF[attId];
return attDef?.primaryManaType === elem.id && attDef.conversionRate > 0;
});
// Calculate total conversion rate TO this element
const totalConversionRate = convertingAttunements.reduce((total, [attId, attState]) => {
return total + getAttunementConversionRate(attId, attState.level || 1);
}, 0);
return (
<div key={elem.id} className="p-3 bg-gray-800/50 rounded border border-gray-700">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-lg">{elem.sym}</span>
<span className="font-semibold" style={{ color: elem.color }}>{elem.name}</span>
<span className="text-xs text-gray-500">({elem.cat})</span>
</div>
<div className="text-sm">
<span className="text-gray-300">{fmt(elem.current)}</span>
<span className="text-gray-500"> / </span>
<span className="text-gray-400">{fmt(elem.max)}</span>
</div>
</div>
{/* Progress bar */}
<div className="w-full bg-gray-700 rounded-full h-2 mb-3">
<div
className="h-2 rounded-full transition-all duration-300"
style={{
width: `${Math.min(100, (elem.current / elem.max) * 100)}%`,
backgroundColor: elem.color
}}
/>
</div>
{/* Conversion info */}
{totalConversionRate > 0 ? (
<div className="text-xs space-y-1">
<div className="flex justify-between">
<span className="text-gray-400">Conversion Rate:</span>
<span className="text-green-400">+{fmtDec(totalConversionRate, 2)}/hr</span>
</div>
{convertingAttunements.length > 0 && (
<div className="text-gray-500 pl-2">
Source: {convertingAttunements.map(([attId]) => {
const attDef = ATTUNEMENTS_DEF[attId];
const level = attunements[attId]?.level || 1;
return `${attDef?.name} (Lv.${level})`;
}).join(', ')}
</div>
)}
</div>
) : (
<div className="text-xs text-gray-500">No active conversion to this element</div>
)}
{/* Show recipe for composite/exotic elements */}
{elem.recipe && (
<div className="text-xs text-gray-500 mt-2 pt-2 border-t border-gray-700">
Recipe: {elem.recipe.map(r => `${ELEMENTS[r]?.sym} ${ELEMENTS[r]?.name || r}`).join(' + ')}
</div>
)}
</div>
);
})}
</div>
</CardContent>
</Card>
);
}
ManaTypeBreakdown.displayName = "ManaTypeBreakdown";
@@ -1,62 +0,0 @@
'use client';
import { fmtDec } from '@/lib/game/stores';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BookOpen } from 'lucide-react';
// Modular stores
import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
export interface StudyStatsSectionProps {
studySpeedMult: number;
studyCostMult: number;
}
export function StudyStatsSection({ studySpeedMult, studyCostMult }: StudyStatsSectionProps) {
// Get state from modular stores
const skills = useSkillStore((s) => s.skills);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
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">+{((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">-{((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 + (skills.knowledgeRetention || 0) * 0.2) * 100)}%</span>
</div>
</div>
</div>
</CardContent>
</Card>
);
}
StudyStatsSection.displayName = "StudyStatsSection";
@@ -1,86 +0,0 @@
'use client';
import { SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
import { SKILLS_DEF } from '@/lib/game/constants';
import type { 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';
// Modular stores
import { useSkillStore } from '@/lib/game/stores';
export function UpgradeEffectsSection() {
// Get state from modular stores
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
// Helper function to get all selected skill upgrades
function getAllSelectedUpgrades() {
const upgrades = [];
for (const [skillId, selectedIds] of Object.entries(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 (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-amber-400 text-sm 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 && 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 && upgrade.effect.type === 'bonus' && (
<div className="text-xs text-blue-400 mt-1">
+{upgrade.effect.value} {upgrade.effect.stat}
</div>
)}
{upgrade.effect && 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.displayName = "UpgradeEffectsSection";
-11
View File
@@ -1,11 +0,0 @@
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';
@@ -11,7 +11,7 @@ import {
computeClickMana, computeClickMana,
getMeditationBonus, getMeditationBonus,
getIncursionStrength, getIncursionStrength,
} from '../computed-stats'; } from '../utils';
import { MAX_DAY, INCURSION_START_DAY, HOURS_PER_TICK } from '../constants'; import { MAX_DAY, INCURSION_START_DAY, HOURS_PER_TICK } from '../constants';
describe('fmt', () => { describe('fmt', () => {
-345
View File
@@ -1,345 +0,0 @@
// ─── Attunement Definitions ─────────────────────────────────────────
// Data file containing all attunement definitions
import type { AttunementDef, AttunementType } from '../types';
export const ATTUNEMENTS: Record<AttunementType, AttunementDef> = {
// ═══════════════════════════════════════════════════════════════════════════
// ENCHANTER - Right Hand
// The starting attunement. Grants access to enchanting and transference magic.
// ═══════════════════════════════════════════════════════════════════════════
enchanter: {
id: 'enchanter',
name: 'Enchanter',
slot: 'rightHand',
description: 'Channel mana through your right hand to imbue equipment with magical properties.',
capability: 'Unlock enchanting. Apply enchantments using transference mana.',
primaryManaType: 'transference',
rawManaRegen: 0.5,
autoConvertRate: 0.2, // 0.2 transference per hour per raw regen
icon: 'Wand2',
color: '#8B5CF6', // Purple
skills: {
// Core enchanting skills
enchanting: {
name: 'Enchanting',
desc: 'Apply magical effects to equipment',
cat: 'enchanter',
max: 10,
base: 100,
studyTime: 8,
},
efficientEnchant: {
name: 'Efficient Enchanting',
desc: 'Reduce enchantment mana costs',
cat: 'enchanter',
max: 5,
base: 200,
studyTime: 12,
req: { enchanting: 3 },
},
enchantSpeed: {
name: 'Swift Enchanting',
desc: 'Faster enchantment application',
cat: 'enchanter',
max: 5,
base: 175,
studyTime: 10,
req: { enchanting: 2 },
},
transferenceMastery: {
name: 'Transference Mastery',
desc: 'Increased transference mana pool and regen',
cat: 'enchanter',
max: 10,
base: 250,
studyTime: 15,
},
},
},
// ... rest of attunement definitions (same as original data.ts)
caster: {
id: 'caster',
name: 'Caster',
slot: 'leftHand',
description: 'Shape mana into devastating spell patterns through your left hand.',
capability: 'Form mana shaping. +25% spell damage bonus.',
primaryManaType: 'form',
rawManaRegen: 0.3,
autoConvertRate: 0.15,
icon: 'Hand',
color: '#3B82F6', // Blue
skills: {
spellShaping: {
name: 'Spell Shaping',
desc: 'Increase spell damage and efficiency',
cat: 'caster',
max: 10,
base: 100,
studyTime: 8,
},
quickCast: {
name: 'Quick Cast',
desc: 'Faster spell casting speed',
cat: 'caster',
max: 10,
base: 120,
studyTime: 8,
},
spellEcho: {
name: 'Spell Echo',
desc: 'Chance to cast spells twice',
cat: 'caster',
max: 5,
base: 300,
studyTime: 15,
req: { spellShaping: 5 },
},
formMastery: {
name: 'Form Mastery',
desc: 'Increased form mana pool and regen',
cat: 'caster',
max: 10,
base: 250,
studyTime: 15,
},
},
},
seer: {
id: 'seer',
name: 'Seer',
slot: 'head',
description: 'See beyond the veil. Reveal hidden truths and enemy weaknesses.',
capability: 'Reveal floor weaknesses. +20% critical hit chance.',
primaryManaType: 'vision',
rawManaRegen: 0.2,
autoConvertRate: 0.1,
icon: 'Eye',
color: '#F59E0B', // Amber
skills: {
insight: {
name: 'Insight',
desc: 'Increased critical hit chance',
cat: 'seer',
max: 10,
base: 100,
studyTime: 8,
},
revealWeakness: {
name: 'Reveal Weakness',
desc: 'Show enemy elemental weaknesses',
cat: 'seer',
max: 5,
base: 200,
studyTime: 12,
},
foresight: {
name: 'Foresight',
desc: 'Chance to anticipate and dodge attacks',
cat: 'seer',
max: 5,
base: 250,
studyTime: 15,
req: { insight: 5 },
},
visionMastery: {
name: 'Vision Mastery',
desc: 'Increased vision mana pool and regen',
cat: 'seer',
max: 10,
base: 250,
studyTime: 15,
},
},
},
warden: {
id: 'warden',
name: 'Warden',
slot: 'back',
description: 'Shield yourself with protective wards and barriers.',
capability: 'Generate protective shields. -10% damage taken.',
primaryManaType: 'barrier',
rawManaRegen: 0.25,
autoConvertRate: 0.12,
icon: 'Shield',
color: '#10B981', // Green
skills: {
warding: {
name: 'Warding',
desc: 'Generate protective shields',
cat: 'warden',
max: 10,
base: 100,
studyTime: 8,
},
fortitude: {
name: 'Fortitude',
desc: 'Reduce damage taken',
cat: 'warden',
max: 10,
base: 150,
studyTime: 10,
},
reflection: {
name: 'Reflection',
desc: 'Chance to reflect damage to attacker',
cat: 'warden',
max: 5,
base: 300,
studyTime: 15,
req: { warding: 5 },
},
barrierMastery: {
name: 'Barrier Mastery',
desc: 'Increased barrier mana pool and regen',
cat: 'warden',
max: 10,
base: 250,
studyTime: 15,
},
},
},
invoker: {
id: 'invoker',
name: 'Invoker',
slot: 'chest',
description: 'Form pacts with spire guardians and channel their elemental power.',
capability: 'Pact with guardians. Gain mana types from pacted guardians.',
primaryManaType: null, // Uses guardian types instead
rawManaRegen: 0.4,
autoConvertRate: 0, // No auto-convert; mana comes from guardian pacts
icon: 'Heart',
color: '#EF4444', // Red
skills: {
pactMaking: {
name: 'Pact Making',
desc: 'Form stronger pacts with guardians',
cat: 'invoker',
max: 10,
base: 100,
studyTime: 8,
},
guardianChannel: {
name: 'Guardian Channeling',
desc: 'Channel guardian powers more effectively',
cat: 'invoker',
max: 10,
base: 150,
studyTime: 10,
},
elementalBurst: {
name: 'Elemental Burst',
desc: 'Unleash stored guardian energy',
cat: 'invoker',
max: 5,
base: 300,
studyTime: 15,
req: { pactMaking: 5, guardianChannel: 3 },
},
soulResonance: {
name: 'Soul Resonance',
desc: 'Deep bond with pacted guardians',
cat: 'invoker',
max: 5,
base: 400,
studyTime: 20,
req: { pactMaking: 8 },
},
},
},
strider: {
id: 'strider',
name: 'Strider',
slot: 'leftLeg',
description: 'Move with supernatural speed and grace.',
capability: 'Enhanced mobility. +15% attack speed.',
primaryManaType: 'flow',
rawManaRegen: 0.3,
autoConvertRate: 0.15,
icon: 'Zap',
color: '#06B6D4', // Cyan
skills: {
swiftness: {
name: 'Swiftness',
desc: 'Increased attack and movement speed',
cat: 'strider',
max: 10,
base: 100,
studyTime: 8,
},
evasive: {
name: 'Evasive',
desc: 'Chance to avoid damage',
cat: 'strider',
max: 5,
base: 200,
studyTime: 12,
},
momentum: {
name: 'Momentum',
desc: 'Build speed over consecutive attacks',
cat: 'strider',
max: 5,
base: 250,
studyTime: 15,
req: { swiftness: 5 },
},
flowMastery: {
name: 'Flow Mastery',
desc: 'Increased flow mana pool and regen',
cat: 'strider',
max: 10,
base: 250,
studyTime: 15,
},
},
},
anchor: {
id: 'anchor',
name: 'Anchor',
slot: 'rightLeg',
description: 'Stand firm against any force. Your foundation is unshakeable.',
capability: 'Increased stability. +100 max mana.',
primaryManaType: 'stability',
rawManaRegen: 0.35,
autoConvertRate: 0.18,
icon: 'Mountain',
color: '#78716C', // Stone gray
skills: {
grounding: {
name: 'Grounding',
desc: 'Increased max mana and stability',
cat: 'anchor',
max: 10,
base: 100,
studyTime: 8,
},
endurance: {
name: 'Endurance',
desc: 'Reduced mana costs when below 50% mana',
cat: 'anchor',
max: 5,
base: 200,
studyTime: 12,
},
ironWill: {
name: 'Iron Will',
desc: 'Prevent mana drain effects',
cat: 'anchor',
max: 5,
base: 250,
studyTime: 15,
req: { grounding: 5 },
},
stabilityMastery: {
name: 'Stability Mastery',
desc: 'Increased stability mana pool and regen',
cat: 'anchor',
max: 10,
base: 250,
studyTime: 15,
},
},
},
};
-30
View File
@@ -1,30 +0,0 @@
// ─── Attunement System ─────────────────────────────────────────────────
// Attunements are powerful magical bonds tied to specific body locations
// Each grants a unique capability, primary mana type, and skill tree
// Re-export types
export type {
AttunementSlot,
AttunementType,
AttunementDef,
AttunementState,
ManaType
} from './types';
export {
ATTUNEMENT_SLOTS,
ATTUNEMENT_SLOT_NAMES
} from './types';
// Re-export data
export { ATTUNEMENTS } from './data';
// Re-export utils
export {
getAttunementForSlot,
getStartingAttunement,
isAttunementUnlocked,
getTotalAttunementRegen,
getManaTypeName,
getManaTypeColor,
} from './utils';
-100
View File
@@ -1,100 +0,0 @@
// ─── Attunement Types ─────────────────────────────────────────────────────────
export type AttunementSlot =
| 'rightHand'
| 'leftHand'
| 'head'
| 'back'
| 'chest'
| 'leftLeg'
| 'rightLeg';
export const ATTUNEMENT_SLOTS: AttunementSlot[] = [
'rightHand',
'leftHand',
'head',
'back',
'chest',
'leftLeg',
'rightLeg',
];
// Slot display names
export const ATTUNEMENT_SLOT_NAMES: Record<AttunementSlot, string> = {
rightHand: 'Right Hand',
leftHand: 'Left Hand',
head: 'Head',
back: 'Back',
chest: 'Heart',
leftLeg: 'Left Leg',
rightLeg: 'Right Leg',
};
// ─── Mana Types ───────────────────────────────────────────────────────────────
export type ManaType =
// Primary mana types from attunements
| 'transference' // Enchanter - moving/enchanting
| 'form' // Caster - shaping spells
| 'vision' // Seer - perception/revelation
| 'barrier' // Warden - protection/defense
| 'flow' // Strider - movement/swiftness
| 'stability' // Anchor - grounding/endurance
// Guardian pact types (Invoker)
| 'fire'
| 'water'
| 'earth'
| 'air'
| 'light'
| 'dark'
| 'life'
| 'death'
// Raw mana
| 'raw';
// ─── Attunement Types ─────────────────────────────────────────────────────────
export type AttunementType =
| 'enchanter'
| 'caster'
| 'seer'
| 'warden'
| 'invoker'
| 'strider'
| 'anchor';
// ─── Attunement Definition ────────────────────────────────────────────────────
export interface AttunementDef {
id: AttunementType;
name: string;
slot: AttunementSlot;
description: string;
capability: string; // What this attunement unlocks
primaryManaType: ManaType | null; // null for Invoker (uses guardian types)
rawManaRegen: number; // Base raw mana regen bonus
autoConvertRate: number; // Raw mana -> primary mana per hour
skills: Record<string, SkillDef>; // Attunement-specific skills
icon: string; // Lucide icon name
color: string; // Theme color
}
// ─── Attunement State ─────────────────────────────────────────────────────────
export interface AttunementState {
unlocked: boolean;
level: number; // Attunement level (from challenges)
manaPool: number; // Current primary mana
maxMana: number; // Max primary mana pool
}
// Skill definition (imported from types but re-defined here for clarity)
export interface SkillDef {
name: string;
desc: string;
cat: string;
max: number;
base: number;
studyTime: number;
req?: Record<string, number>;
}
-94
View File
@@ -1,94 +0,0 @@
// ─── Attunement Helper Functions ─────────────────────────
import type { AttunementSlot, AttunementType, AttunementState, ManaType, AttunementDef } from './types';
import { ATTUNEMENTS } from './data';
/**
* Get the attunement for a specific body slot
*/
export function getAttunementForSlot(slot: AttunementSlot): AttunementDef | undefined {
return Object.values(ATTUNEMENTS).find(a => a.slot === slot) as AttunementDef | undefined;
}
/**
* Get the starting attunement (Enchanter - right hand)
*/
export function getStartingAttunement(): AttunementDef {
return ATTUNEMENTS.enchanter;
}
/**
* Check if an attunement is unlocked for the player
*/
export function isAttunementUnlocked(
attunementStates: Record<AttunementType, AttunementState>,
attunementType: AttunementType
): boolean {
return attunementStates[attunementType]?.unlocked ?? false;
}
/**
* Get total raw mana regen from all unlocked attunements
*/
export function getTotalAttunementRegen(
attunementStates: Record<AttunementType, AttunementState>
): number {
let total = 0;
for (const [type, state] of Object.entries(attunementStates)) {
if (state.unlocked) {
const def = ATTUNEMENTS[type as AttunementType];
if (def) {
total += def.rawManaRegen * (1 + state.level * 0.1); // +10% per level
}
}
}
return total;
}
/**
* Get mana type display name
*/
export function getManaTypeName(type: ManaType): string {
const names: Record<ManaType, string> = {
raw: 'Raw Mana',
transference: 'Transference',
form: 'Form',
vision: 'Vision',
barrier: 'Barrier',
flow: 'Flow',
stability: 'Stability',
fire: 'Fire',
water: 'Water',
earth: 'Earth',
air: 'Air',
light: 'Light',
dark: 'Dark',
life: 'Life',
death: 'Death',
};
return names[type] || type;
}
/**
* Get mana type color
*/
export function getManaTypeColor(type: ManaType): string {
const colors: Record<ManaType, string> = {
raw: '#A78BFA', // Light purple
transference: '#8B5CF6', // Purple
form: '#3B82F6', // Blue
vision: '#F59E0B', // Amber
barrier: '#10B981', // Green
flow: '#06B6D4', // Cyan
stability: '#78716C', // Stone
fire: '#EF4444', // Red
water: '#3B82F6', // Blue
earth: '#A16207', // Brown
air: '#94A3B8', // Slate
light: '#FCD34D', // Yellow
dark: '#6B7280', // Gray
life: '#22C55E', // Green
death: '#7C3AED', // Violet
};
return colors[type] || '#A78BFA';
}
-12
View File
@@ -1,12 +0,0 @@
// ─── Computed Stats and Utility Functions ───────────────────────────────────────
// This module now re-exports from focused utility modules for better organization
//
// The functions have been split into:
// - ./utils/formatting.ts - Number formatting (fmt, fmtDec)
// - ./utils/floor-utils.ts - Floor functions (getFloorMaxHP, getFloorElement)
// - ./utils/mana-utils.ts - Mana calculations (computeMaxMana, computeElementMax, etc.)
// - ./utils/combat-utils.ts - Combat functions (calcDamage, calcInsight, getTotalDPS, etc.)
//
// All exports are maintained for backward compatibility.
export * from './utils/index';
-12
View File
@@ -1,12 +0,0 @@
/**
* Helper to get unified effects from game state
*/
export function getUnifiedEffects(state: Pick<GameState, 'skillUpgrades' | 'skillTiers' | 'equipmentInstances' | 'equippedInstances'>): UnifiedEffects {
return computeAllEffects(
state.skillUpgrades || {},
state.skillTiers || {},
state.equipmentInstances || {},
state.equippedInstances || {}
);
}
-46
View File
@@ -1,46 +0,0 @@
// ─── Shared Formatting Utilities ─────────────────────────────────────────────────
// Utility functions for consistent formatting across components
import { ELEMENTS } from '@/lib/game/constants';
import type { SpellCost } from '@/lib/game/types';
// Re-export number formatting functions from computed-stats.ts
export { fmt, fmtDec } from './computed-stats';
/**
* Format a spell cost for display
*/
export function formatSpellCost(cost: SpellCost): string {
if (cost.type === 'raw') {
return `${cost.amount} raw`;
}
const elemDef = ELEMENTS[cost.element || ''];
return `${cost.amount} ${elemDef?.sym || '?'}`;
}
/**
* Get the display color for a spell cost
*/
export function getSpellCostColor(cost: SpellCost): string {
if (cost.type === 'raw') {
return '#60A5FA'; // Blue for raw mana
}
return ELEMENTS[cost.element || '']?.color || '#9CA3AF';
}
/**
* Format study time in hours to human-readable string
*/
export function formatStudyTime(hours: number): string {
if (hours < 1) return `${Math.round(hours * 60)}m`;
return `${hours.toFixed(1)}h`;
}
/**
* Format time (hour of day) to HH:MM format
*/
export function formatHour(hour: number): string {
const h = Math.floor(hour);
const m = Math.floor((hour % 1) * 60);
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
}
-75
View File
@@ -1,75 +0,0 @@
// ─── Navigation Slice ─────────────────────────────────────────────────────────
// Actions for floor navigation: climbing direction and manual floor changes
import type { GameState } from './types';
import { getFloorMaxHP } from './computed-stats';
// ─── Navigation Actions Interface ─────────────────────────────────────────────
export interface NavigationActions {
// Floor Navigation
setClimbDirection: (direction: 'up' | 'down') => void;
changeFloor: (direction: 'up' | 'down') => void;
resetFloorHP: () => void;
}
// ─── Navigation Slice Factory ─────────────────────────────────────────────────
export function createNavigationSlice(
set: (partial: Partial<GameState> | ((state: GameState) => Partial<GameState>)) => void,
get: () => GameState
): NavigationActions {
return {
// Set the climbing direction (up or down)
setClimbDirection: (direction: 'up' | 'down') => {
set({ climbDirection: direction });
},
// Manually change floors by one
changeFloor: (direction: 'up' | 'down') => {
const state = get();
const currentFloor = state.currentFloor;
// Calculate next floor
const nextFloor = direction === 'up'
? Math.min(currentFloor + 1, 100)
: Math.max(currentFloor - 1, 1);
// Can't stay on same floor
if (nextFloor === currentFloor) return;
// Mark current floor as cleared (it will respawn when we come back)
const clearedFloors = { ...state.clearedFloors };
clearedFloors[currentFloor] = true;
// Check if next floor was cleared (needs respawn)
const nextFloorCleared = clearedFloors[nextFloor];
if (nextFloorCleared) {
// Respawn the floor
delete clearedFloors[nextFloor];
}
set({
currentFloor: nextFloor,
floorMaxHP: getFloorMaxHP(nextFloor),
floorHP: getFloorMaxHP(nextFloor),
maxFloorReached: Math.max(state.maxFloorReached, nextFloor),
clearedFloors,
climbDirection: direction,
equipmentSpellStates: state.equipmentSpellStates.map(s => ({ ...s, castProgress: 0 })),
log: [`🚶 Moved to floor ${nextFloor}${nextFloorCleared ? ' (respawned)' : ''}.`, ...state.log.slice(0, 49)],
});
},
// Reset current floor HP to max (useful when floor HP gets stuck)
resetFloorHP: () => {
const state = get();
const maxHP = getFloorMaxHP(state.currentFloor);
set({
floorMaxHP: maxHP,
floorHP: maxHP,
log: [`🔄 Floor ${state.currentFloor} HP reset to full.`, ...state.log.slice(0, 49)],
});
},
};
}
+23 -48
View File
@@ -5,50 +5,36 @@
import type { GameState, SpellCost, StudyTarget } from '../types'; import type { GameState, SpellCost, StudyTarget } from '../types';
import type { ComputedEffects } from '../upgrade-effects.types'; import type { ComputedEffects } from '../upgrade-effects.types';
import type { UnifiedEffects } from '../effects'; import type { UnifiedEffects } from '../effects';
import { SPELLS_DEF, GUARDIANS, ELEMENT_OPPOSITES, SKILLS_DEF, HOURS_PER_TICK, TICK_MS, INCURSION_START_DAY, MAX_DAY, ELEMENTS } from '../constants'; import { SPELLS_DEF, GUARDIANS, ELEMENT_OPPOSITES, HOURS_PER_TICK, TICK_MS, INCURSION_START_DAY, MAX_DAY, ELEMENTS } from '../constants';
import { getUnifiedEffects } from '../effects'; import { getUnifiedEffects } from '../effects';
import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements'; import { getTotalAttunementRegen, getTotalAttunementConversionDrain } from '../data/attunements';
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects'; import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
// Helper to get effective skill level accounting for tiers
function getEffectiveSkillLevel(
skills: Record<string, number>,
baseSkillId: string,
skillTiers: Record<string, number> = {}
): { level: number; tier: number; tierMultiplier: number } {
const currentTier = skillTiers[baseSkillId] || 1;
const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId;
const level = skills[tieredSkillId] || skills[baseSkillId] || 0;
const tierMultiplier = Math.pow(10, currentTier - 1);
return { level, tier: currentTier, tierMultiplier };
}
export function computeMaxMana( export function computeMaxMana(
state: GameState, state: GameState,
effects?: ComputedEffects | UnifiedEffects effects?: ComputedEffects | UnifiedEffects
): number { ): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const skillMult = (effects as any)?.skillLevelMultiplier || 1; const base = 100 + ((pu || {}).manaWell || 0) * 500;
const base = 100 + ((state.skills || {}).manaWell || 0) * 100 * skillMult + ((pu || {}).manaWell || 0) * 500;
// Check if we need to compute effects from equipment // Check if we need to compute effects from equipment
if (!effects && state.equipmentInstances && state.equippedInstances) { if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any); effects = getUnifiedEffects(state as any);
} }
let maxMana: number; let maxMana: number;
if (effects) { if (effects) {
maxMana = Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier); maxMana = Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier);
} else { } else {
maxMana = base; maxMana = base;
} }
if (effects && hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDENSE)) { if (effects && hasSpecial(effects, SPECIAL_EFFECTS.MANA_CONDENSE)) {
const totalGathered = state.totalManaGathered || 0; const totalGathered = state.totalManaGathered || 0;
const condensesBonus = Math.floor(totalGathered / 1000); const condensesBonus = Math.floor(totalGathered / 1000);
maxMana = Math.floor(maxMana * (1 + condensesBonus * 0.01)); maxMana = Math.floor(maxMana * (1 + condensesBonus * 0.01));
} }
return maxMana; return maxMana;
} }
@@ -58,15 +44,15 @@ export function computeElementMax(
element?: string element?: string
): number { ): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25; const base = 10 + (pu.elementalAttune || 0) * 25;
let adjustedBase = base; let adjustedBase = base;
if (element && state.unlockedManaTypeUpgrades) { if (element && state.unlockedManaTypeUpgrades) {
const typeUpgrades = state.unlockedManaTypeUpgrades.filter(u => u.typeId === element); const typeUpgrades = state.unlockedManaTypeUpgrades.filter(u => u.typeId === element);
const totalLevels = typeUpgrades.reduce((sum, u) => sum + u.level, 0); const totalLevels = typeUpgrades.reduce((sum, u) => sum + u.level, 0);
adjustedBase = base + (totalLevels * 10); adjustedBase = base + (totalLevels * 10);
} }
if (effects) { if (effects) {
let bonus = effects.elementCapBonus || 0; let bonus = effects.elementCapBonus || 0;
if (element && (effects as UnifiedEffects).perElementCapBonus) { if (element && (effects as UnifiedEffects).perElementCapBonus) {
@@ -86,17 +72,16 @@ export function computeRegen(
): number { ): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1; const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1;
const skillMult = (effects as any)?.skillLevelMultiplier || 1; const base = 2 + (pu.manaFlow || 0) * 0.5;
const base = 2 + (state.skills.manaFlow || 0) * 1 * skillMult + (state.skills.manaSpring || 0) * 2 * skillMult + (pu.manaFlow || 0) * 0.5;
let regen = base * temporalBonus; let regen = base * temporalBonus;
const attunementRegen = getTotalAttunementRegen(state.attunements || {}); const attunementRegen = getTotalAttunementRegen(state.attunements || {});
regen += attunementRegen; regen += attunementRegen;
if (!effects && state.equipmentInstances && state.equippedInstances) { if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any); effects = getUnifiedEffects(state as any);
} }
if (effects) { if (effects) {
regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier; regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier;
} }
@@ -127,13 +112,12 @@ export function computeClickMana(
state: GameState, state: GameState,
effects?: ComputedEffects | UnifiedEffects effects?: ComputedEffects | UnifiedEffects
): number { ): number {
const skillMult = (effects as any)?.skillLevelMultiplier || 1; const base = 1;
const base = 1 + (state.skills.manaTap || 0) * 1 * skillMult + (state.skills.manaSurge || 0) * 3 * skillMult;
if (!effects && state.equipmentInstances && state.equippedInstances) { if (!effects && state.equipmentInstances && state.equippedInstances) {
effects = getUnifiedEffects(state as any); effects = getUnifiedEffects(state as any);
} }
if (effects) { if (effects) {
return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier); return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier);
} }
@@ -156,14 +140,12 @@ export function calcDamage(
): number { ): number {
const sp = SPELLS_DEF[spellId]; const sp = SPELLS_DEF[spellId];
if (!sp) return 5; if (!sp) return 5;
const skills = state.skills; const baseDmg = sp.dmg;
const skillMult = (effects as any)?.skillLevelMultiplier || 1; const pct = 1;
const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5 * skillMult; const elemMasteryBonus = 1;
const pct = 1 + (skills.arcaneFury || 0) * 0.1 * skillMult; const critChance = 0;
const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15 * skillMult;
const critChance = (skills.precision || 0) * 0.05;
const pactMult = state.signedPacts.reduce((m, f) => m * ((GUARDIANS as any)[f]?.pact || 1), 1); const pactMult = state.signedPacts.reduce((m, f) => m * ((GUARDIANS as any)[f]?.pact || 1), 1);
let damage = baseDmg * pct * pactMult * elemMasteryBonus; let damage = baseDmg * pct * pactMult * elemMasteryBonus;
if (floorElem) { if (floorElem) {
damage *= getElementalBonus(sp.elem, floorElem); damage *= getElementalBonus(sp.elem, floorElem);
@@ -176,20 +158,13 @@ export function calcDamage(
export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number { export function calcInsight(state: Pick<GameState, 'maxFloorReached' | 'totalManaGathered' | 'signedPacts' | 'prestigeUpgrades' | 'skills'>): number {
const pu = state.prestigeUpgrades; const pu = state.prestigeUpgrades;
const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1; const mult = (1 + (pu.insightAmp || 0) * 0.25);
const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus;
return Math.floor((state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult); return Math.floor((state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult);
} }
export function getMeditationBonus(meditateTicks: number, skills: Record<string, number>, meditationEfficiency: number = 1): number { export function getMeditationBonus(meditateTicks: number, meditationEfficiency: number = 1): number {
const hasMeditation = skills.meditation === 1;
const hasDeepTrance = skills.deepTrance === 1;
const hasVoidMeditation = skills.voidMeditation === 1;
const hours = meditateTicks * HOURS_PER_TICK; const hours = meditateTicks * HOURS_PER_TICK;
let bonus = 1 + Math.min(hours / 4, 0.5); let bonus = 1 + Math.min(hours / 4, 0.5);
if (hasMeditation && hours >= 4) bonus = 2.5;
if (hasDeepTrance && hours >= 6) bonus = 3.0;
if (hasVoidMeditation && hours >= 8) bonus = 5.0;
bonus *= meditationEfficiency; bonus *= meditationEfficiency;
return bonus; return bonus;
} }
-9
View File
@@ -1,9 +0,0 @@
// ─── Store Module Exports ─────────────────────────────────────────────────────
// Re-exports from main store and adds new computed utilities
// This allows gradual migration while keeping existing functionality
// Re-export everything from the main store
export * from '../store';
// Export new computed utilities
export * from './computed';
+7 -9
View File
@@ -10,7 +10,6 @@ import { usePrestigeStore } from './prestigeStore';
export function processCombatTick( export function processCombatTick(
get: () => CombatState, get: () => CombatState,
set: (state: Partial<CombatState>) => void, set: (state: Partial<CombatState>) => void,
skills: Record<string, number>,
rawMana: number, rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>, elements: Record<string, { current: number; max: number; unlocked: boolean }>,
maxMana: number, maxMana: number,
@@ -36,9 +35,8 @@ export function processCombatTick(
return { rawMana, elements, logMessages, totalManaGathered }; return { rawMana, elements, logMessages, totalManaGathered };
} }
// Calculate cast speed // Calculate cast speed (no skill bonus)
const baseAttackSpeed = 1 + (skills.quickCast || 0) * 0.05; const totalAttackSpeed = attackSpeedMult;
const totalAttackSpeed = baseAttackSpeed * attackSpeedMult;
const spellCastSpeed = spellDef.castSpeed || 1; const spellCastSpeed = spellDef.castSpeed || 1;
const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed; const progressPerTick = HOURS_PER_TICK * spellCastSpeed * totalAttackSpeed;
@@ -58,12 +56,12 @@ export function processCombatTick(
// Calculate base damage // Calculate base damage
const floorElement = getFloorElement(currentFloor); const floorElement = getFloorElement(currentFloor);
const damage = calcDamage( const damage = calcDamage(
{ skills, signedPacts: usePrestigeStore.getState().signedPacts }, { skills: {}, signedPacts: usePrestigeStore.getState().signedPacts },
spellId, spellId,
floorElement, floorElement,
); );
// Let gameStore apply damage modifiers (executioner, berserker, spell echo) // Let gameStore apply damage modifiers (executioner, berserker)
const result = onDamageDealt(damage); const result = onDamageDealt(damage);
rawMana = result.rawMana; rawMana = result.rawMana;
elements = result.elements; elements = result.elements;
@@ -114,7 +112,7 @@ export function processCombatTick(
// Calculate damage // Calculate damage
const eFloorElement = getFloorElement(currentFloor); const eFloorElement = getFloorElement(currentFloor);
const eDamage = calcDamage( const eDamage = calcDamage(
{ skills, signedPacts: usePrestigeStore.getState().signedPacts }, { skills: {}, signedPacts: usePrestigeStore.getState().signedPacts },
eSpell.spellId, eSpell.spellId,
eFloorElement, eFloorElement,
); );
@@ -151,11 +149,11 @@ export function makeInitialSpells(spellsToKeep: string[] = []): Record<string, S
const startSpells: Record<string, SpellState> = { const startSpells: Record<string, SpellState> = {
manaBolt: { learned: true, level: 1, studyProgress: 0 }, manaBolt: { learned: true, level: 1, studyProgress: 0 },
}; };
// Add kept spells // Add kept spells
for (const spellId of spellsToKeep) { for (const spellId of spellsToKeep) {
startSpells[spellId] = { learned: true, level: 1, studyProgress: 0 }; startSpells[spellId] = { learned: true, level: 1, studyProgress: 0 };
} }
return startSpells; return startSpells;
} }
+68 -71
View File
@@ -17,60 +17,60 @@ export interface CombatState {
floorHP: number; floorHP: number;
floorMaxHP: number; floorMaxHP: number;
maxFloorReached: number; maxFloorReached: number;
// Action state // Action state
activeSpell: string; activeSpell: string;
currentAction: GameAction; currentAction: GameAction;
castProgress: number; castProgress: number;
// Spire mode // Spire mode
spireMode: boolean; spireMode: boolean;
// Room system for special floors // Room system for special floors
currentRoom: FloorState; currentRoom: FloorState;
// Spire climbing state // Spire climbing state
clearedFloors: Record<number, boolean>; clearedFloors: Record<number, boolean>;
climbDirection: 'up' | 'down' | null; climbDirection: 'up' | 'down' | null;
isDescending: boolean; isDescending: boolean;
// Golemancy (summoned golems) // Golemancy (summoned golems)
golemancy: GolemancyState; golemancy: GolemancyState;
// Equipment spell states for multi-casting // Equipment spell states for multi-casting
equipmentSpellStates: EquipmentSpellState[]; equipmentSpellStates: EquipmentSpellState[];
// Combat special effect tracking // Combat special effect tracking
comboHitCount: number; comboHitCount: number;
floorHitCount: number; floorHitCount: number;
// Spells // Spells
spells: Record<string, SpellState>; spells: Record<string, SpellState>;
// Activity Log (for Spire Mode UI) // Activity Log (for Spire Mode UI)
activityLog: ActivityLogEntry[]; activityLog: ActivityLogEntry[];
// Achievements // Achievements
achievements: AchievementState; achievements: AchievementState;
// Stats tracking // Stats tracking
totalSpellsCast: number; totalSpellsCast: number;
totalDamageDealt: number; totalDamageDealt: number;
totalCraftsCompleted: number; totalCraftsCompleted: number;
// Actions // Actions
setCurrentFloor: (floor: number) => void; setCurrentFloor: (floor: number) => void;
advanceFloor: () => void; advanceFloor: () => void;
setFloorHP: (hp: number) => void; setFloorHP: (hp: number) => void;
setMaxFloorReached: (floor: number) => void; setMaxFloorReached: (floor: number) => void;
setAction: (action: GameAction) => void; setAction: (action: GameAction) => void;
setSpell: (spellId: string) => void; setSpell: (spellId: string) => void;
setCastProgress: (progress: number) => void; setCastProgress: (progress: number) => void;
// Room state // Room state
setCurrentRoom: (room: FloorState) => void; setCurrentRoom: (room: FloorState) => void;
// Spire climbing // Spire climbing
setClimbDirection: (direction: 'up' | 'down' | null) => void; setClimbDirection: (direction: 'up' | 'down' | null) => void;
setClearedFloor: (floor: number, cleared: boolean) => void; setClearedFloor: (floor: number, cleared: boolean) => void;
@@ -79,29 +79,28 @@ export interface CombatState {
exitSpireMode: () => void; exitSpireMode: () => void;
startClimbUp: () => void; startClimbUp: () => void;
startClimbDown: () => void; startClimbDown: () => void;
// Golemancy // Golemancy
toggleGolem: (golemId: string) => void; toggleGolem: (golemId: string) => void;
setEnabledGolems: (golemIds: string[]) => void; setEnabledGolems: (golemIds: string[]) => void;
// Spells // Spells
learnSpell: (spellId: string) => void; learnSpell: (spellId: string) => void;
setSpellState: (spellId: string, state: Partial<SpellState>) => void; setSpellState: (spellId: string, state: Partial<SpellState>) => void;
// Activity Log // Activity Log
addActivityLog: (eventType: ActivityEventType, message: string, details?: ActivityLogEntry['details']) => void; addActivityLog: (eventType: ActivityEventType, message: string, details?: ActivityLogEntry['details']) => void;
// Stats // Stats
incrementSpellsCast: () => void; incrementSpellsCast: () => void;
addDamageDealt: (damage: number) => void; addDamageDealt: (damage: number) => void;
incrementCraftsCompleted: () => void; incrementCraftsCompleted: () => void;
// Spire mode // Spire mode
enterSpireMode: () => void; enterSpireMode: () => void;
// Combat tick // Combat tick
processCombatTick: ( processCombatTick: (
skills: Record<string, number>,
rawMana: number, rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>, elements: Record<string, { current: number; max: number; unlocked: boolean }>,
maxMana: number, maxMana: number,
@@ -109,10 +108,10 @@ export interface CombatState {
onFloorCleared: (floor: number, wasGuardian: boolean) => void, onFloorCleared: (floor: number, wasGuardian: boolean) => void,
onDamageDealt: (damage: number) => { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> }, onDamageDealt: (damage: number) => { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }> },
) => { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }>; logMessages: string[]; totalManaGathered: number }; ) => { rawMana: number; elements: Record<string, { current: number; max: number; unlocked: boolean }>; logMessages: string[]; totalManaGathered: number };
// Reset // Reset
resetCombat: (startFloor: number, spellsToKeep?: string[]) => void; resetCombat: (startFloor: number, spellsToKeep?: string[]) => void;
// Debug helpers // Debug helpers
debugSetFloor: (floor: number) => void; debugSetFloor: (floor: number) => void;
resetFloorHP: () => void; resetFloorHP: () => void;
@@ -130,48 +129,48 @@ export const useCombatStore = create<CombatState>()(
currentAction: 'meditate', currentAction: 'meditate',
castProgress: 0, castProgress: 0,
spireMode: false, spireMode: false,
// Room system // Room system
currentRoom: generateFloorState(1), currentRoom: generateFloorState(1),
// Spire climbing state // Spire climbing state
clearedFloors: {}, clearedFloors: {},
climbDirection: null, climbDirection: null,
isDescending: false, isDescending: false,
// Golemancy // Golemancy
golemancy: { golemancy: {
enabledGolems: [], enabledGolems: [],
summonedGolems: [], summonedGolems: [],
lastSummonFloor: 0, lastSummonFloor: 0,
}, },
// Equipment spell states // Equipment spell states
equipmentSpellStates: [], equipmentSpellStates: [],
// Combat tracking // Combat tracking
comboHitCount: 0, comboHitCount: 0,
floorHitCount: 0, floorHitCount: 0,
// Spells // Spells
spells: { spells: {
manaBolt: { learned: true, level: 1, studyProgress: 0 }, manaBolt: { learned: true, level: 1, studyProgress: 0 },
}, },
// Activity Log // Activity Log
activityLog: [], activityLog: [],
// Achievements // Achievements
achievements: { achievements: {
unlocked: [], unlocked: [],
progress: {}, progress: {},
}, },
// Stats tracking // Stats tracking
totalSpellsCast: 0, totalSpellsCast: 0,
totalDamageDealt: 0, totalDamageDealt: 0,
totalCraftsCompleted: 0, totalCraftsCompleted: 0,
setCurrentFloor: (floor: number) => { setCurrentFloor: (floor: number) => {
set({ set({
currentFloor: floor, currentFloor: floor,
@@ -179,7 +178,7 @@ export const useCombatStore = create<CombatState>()(
floorMaxHP: getFloorMaxHP(floor), floorMaxHP: getFloorMaxHP(floor),
}); });
}, },
advanceFloor: () => { advanceFloor: () => {
set((state) => { set((state) => {
const newFloor = Math.min(state.currentFloor + 1, 100); const newFloor = Math.min(state.currentFloor + 1, 100);
@@ -192,52 +191,52 @@ export const useCombatStore = create<CombatState>()(
}; };
}); });
}, },
setFloorHP: (hp: number) => { setFloorHP: (hp: number) => {
set({ floorHP: Math.max(0, hp) }); set({ floorHP: Math.max(0, hp) });
}, },
setMaxFloorReached: (floor: number) => { setMaxFloorReached: (floor: number) => {
set((state) => ({ set((state) => ({
maxFloorReached: Math.max(state.maxFloorReached, floor), maxFloorReached: Math.max(state.maxFloorReached, floor),
})); }));
}, },
setAction: (action: GameAction) => { setAction: (action: GameAction) => {
set({ currentAction: action }); set({ currentAction: action });
}, },
setSpell: (spellId: string) => { setSpell: (spellId: string) => {
const state = get(); const state = get();
if (state.spells[spellId]?.learned) { if (state.spells[spellId]?.learned) {
set({ activeSpell: spellId }); set({ activeSpell: spellId });
} }
}, },
setCastProgress: (progress: number) => { setCastProgress: (progress: number) => {
set({ castProgress: progress }); set({ castProgress: progress });
}, },
// Room state // Room state
setCurrentRoom: (room: FloorState) => { setCurrentRoom: (room: FloorState) => {
set({ currentRoom: room }); set({ currentRoom: room });
}, },
// Spire climbing // Spire climbing
setClimbDirection: (direction: 'up' | 'down' | null) => { setClimbDirection: (direction: 'up' | 'down' | null) => {
set({ climbDirection: direction }); set({ climbDirection: direction });
}, },
setClearedFloor: (floor: number, cleared: boolean) => { setClearedFloor: (floor: number, cleared: boolean) => {
set((state) => ({ set((state) => ({
clearedFloors: { ...state.clearedFloors, [floor]: cleared }, clearedFloors: { ...state.clearedFloors, [floor]: cleared },
})); }));
}, },
setIsDescending: (descending: boolean) => { setIsDescending: (descending: boolean) => {
set({ isDescending: descending }); set({ isDescending: descending });
}, },
climbDownFloor: () => { climbDownFloor: () => {
set((s) => { set((s) => {
if (s.currentFloor <= 1) return s; if (s.currentFloor <= 1) return s;
@@ -251,41 +250,41 @@ export const useCombatStore = create<CombatState>()(
}; };
}); });
}, },
exitSpireMode: () => { exitSpireMode: () => {
set({ spireMode: false, currentAction: 'meditate', climbDirection: null, isDescending: false }); set({ spireMode: false, currentAction: 'meditate', climbDirection: null, isDescending: false });
}, },
startClimbUp: () => set({ climbDirection: 'up', currentAction: 'climb' }), startClimbUp: () => set({ climbDirection: 'up', currentAction: 'climb' }),
startClimbDown: () => set({ climbDirection: 'down', currentAction: 'climb' }), startClimbDown: () => set({ climbDirection: 'down', currentAction: 'climb' }),
// Golemancy // Golemancy
toggleGolem: (golemId: string) => { toggleGolem: (golemId: string) => {
set((s) => { set((s) => {
const enabledGolems = s.golemancy?.enabledGolems || []; const enabledGolems = s.golemancy?.enabledGolems || [];
const isEnabled = enabledGolems.includes(golemId); const isEnabled = enabledGolems.includes(golemId);
return { return {
golemancy: { golemancy: {
...s.golemancy, ...s.golemancy,
enabledGolems: isEnabled enabledGolems: isEnabled
? enabledGolems.filter(id => id !== golemId) ? enabledGolems.filter(id => id !== golemId)
: [...enabledGolems, golemId] : [...enabledGolems, golemId]
}, },
}; };
}); });
}, },
setEnabledGolems: (golemIds: string[]) => { setEnabledGolems: (golemIds: string[]) => {
set((s) => ({ set((s) => ({
golemancy: { ...s.golemancy, enabledGolems: golemIds }, golemancy: { ...s.golemancy, enabledGolems: golemIds },
})); }));
}, },
enterSpireMode: () => { enterSpireMode: () => {
set({ spireMode: true }); set({ spireMode: true });
}, },
learnSpell: (spellId: string) => { learnSpell: (spellId: string) => {
set((state) => ({ set((state) => ({
spells: { spells: {
@@ -294,7 +293,7 @@ export const useCombatStore = create<CombatState>()(
}, },
})); }));
}, },
setSpellState: (spellId: string, spellState: Partial<SpellState>) => { setSpellState: (spellId: string, spellState: Partial<SpellState>) => {
set((state) => ({ set((state) => ({
spells: { spells: {
@@ -303,29 +302,28 @@ export const useCombatStore = create<CombatState>()(
}, },
})); }));
}, },
// Activity Log // Activity Log
addActivityLog: (eventType: ActivityEventType, message: string, details?: ActivityLogEntry['details']) => { addActivityLog: (eventType: ActivityEventType, message: string, details?: ActivityLogEntry['details']) => {
set((state) => ({ set((state) => ({
activityLog: addActivityLogEntry(state, eventType, message, details), activityLog: addActivityLogEntry(state, eventType, message, details),
})); }));
}, },
// Stats // Stats
incrementSpellsCast: () => { incrementSpellsCast: () => {
set((state) => ({ totalSpellsCast: state.totalSpellsCast + 1 })); set((state) => ({ totalSpellsCast: state.totalSpellsCast + 1 }));
}, },
addDamageDealt: (damage: number) => { addDamageDealt: (damage: number) => {
set((state) => ({ totalDamageDealt: state.totalDamageDealt + damage })); set((state) => ({ totalDamageDealt: state.totalDamageDealt + damage }));
}, },
incrementCraftsCompleted: () => { incrementCraftsCompleted: () => {
set((state) => ({ totalCraftsCompleted: state.totalCraftsCompleted + 1 })); set((state) => ({ totalCraftsCompleted: state.totalCraftsCompleted + 1 }));
}, },
processCombatTick: ( processCombatTick: (
skills: Record<string, number>,
rawMana: number, rawMana: number,
elements: Record<string, { current: number; max: number; unlocked: boolean }>, elements: Record<string, { current: number; max: number; unlocked: boolean }>,
maxMana: number, maxMana: number,
@@ -336,7 +334,6 @@ export const useCombatStore = create<CombatState>()(
return processCombatTick( return processCombatTick(
get, get,
set, set,
skills,
rawMana, rawMana,
elements, elements,
maxMana, maxMana,
@@ -345,10 +342,10 @@ export const useCombatStore = create<CombatState>()(
onDamageDealt, onDamageDealt,
); );
}, },
resetCombat: (startFloor: number, spellsToKeep: string[] = []) => { resetCombat: (startFloor: number, spellsToKeep: string[] = []) => {
const startSpells = makeInitialSpells(spellsToKeep); const startSpells = makeInitialSpells(spellsToKeep);
set({ set({
currentFloor: startFloor, currentFloor: startFloor,
floorHP: getFloorMaxHP(startFloor), floorHP: getFloorMaxHP(startFloor),
@@ -360,7 +357,7 @@ export const useCombatStore = create<CombatState>()(
spells: startSpells, spells: startSpells,
}); });
}, },
// Debug helpers // Debug helpers
debugSetFloor: (floor: number) => { debugSetFloor: (floor: number) => {
set({ set({
@@ -369,13 +366,13 @@ export const useCombatStore = create<CombatState>()(
floorMaxHP: getFloorMaxHP(floor), floorMaxHP: getFloorMaxHP(floor),
}); });
}, },
resetFloorHP: () => { resetFloorHP: () => {
set((state) => ({ set((state) => ({
floorHP: state.floorMaxHP, floorHP: state.floorMaxHP,
})); }));
}, },
debugSetTime: (day: number, hour: number) => { debugSetTime: (day: number, hour: number) => {
useGameStore.setState({ day, hour }); useGameStore.setState({ day, hour });
}, },
@@ -386,7 +383,7 @@ export const useCombatStore = create<CombatState>()(
currentFloor: state.currentFloor, currentFloor: state.currentFloor,
maxFloorReached: state.maxFloorReached, maxFloorReached: state.maxFloorReached,
spells: state.spells, spells: state.spells,
activeSpell: state.activeSpell, activeSpell: state.activeAction,
}), }),
} }
) )
+3 -35
View File
@@ -1,28 +1,14 @@
// ─── Crafting Store ───────────────────────────────────────────────────── // ─── Crafting Store ─────────────────────────────────────────────────────
// Handles equipment crafting, enchantment design, and crafting progress
// This is a modular store that manages all crafting-related state
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import type { DesignProgress, PreparationProgress, ApplicationProgress, EquipmentCraftingProgress, EnchantmentDesign, EquipmentInstance, DesignEffect } from '../types'; import type { DesignProgress, PreparationProgress, ApplicationProgress, EquipmentCraftingProgress, EnchantmentDesign, EquipmentInstance, DesignEffect } from '../types';
// Import crafting modules for action logic
import * as CraftingUtils from '../crafting-utils'; import * as CraftingUtils from '../crafting-utils';
import * as CraftingDesign from '../crafting-design'; import * as CraftingDesign from '../crafting-design';
import { computeEffects } from '../upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
// Import other stores to access required state
import { useSkillStore } from './skillStore';
import { useGameStore } from './gameStore';
import { useManaStore } from './manaStore'; import { useManaStore } from './manaStore';
import { useCombatStore } from './combatStore'; import { useCombatStore } from './combatStore';
import { createStartingEquipment } from '../store/crafting-modules/starting-equipment'; import { createStartingEquipment } from '../crafting-slice';
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
// Import action modules
import * as ApplicationActions from '../crafting-actions/application-actions'; import * as ApplicationActions from '../crafting-actions/application-actions';
import * as CraftingApply from '../crafting-apply';
import * as PreparationActions from '../crafting-actions/preparation-actions'; import * as PreparationActions from '../crafting-actions/preparation-actions';
import * as CraftingEquipment from '../crafting-equipment'; import * as CraftingEquipment from '../crafting-equipment';
@@ -142,25 +128,18 @@ export const useCraftingStore = create<CraftingStore>()(
// Enchantment design actions // Enchantment design actions
startDesigningEnchantment: (name, equipmentTypeId, effects) => { startDesigningEnchantment: (name, equipmentTypeId, effects) => {
// Get state from other stores
const skillState = useSkillStore.getState();
const state = get(); // crafting state const state = get(); // crafting state
const enchantingLevel = skillState.skills?.enchanting || 0; const validation = CraftingDesign.validateDesignEffects(effects, equipmentTypeId, 0);
const validation = CraftingDesign.validateDesignEffects(effects, equipmentTypeId, enchantingLevel);
if (!validation.valid) return false; if (!validation.valid) return false;
const equipType = CraftingUtils.getEquipmentType(equipmentTypeId); const equipType = CraftingUtils.getEquipmentType(equipmentTypeId);
if (!equipType) return false; if (!equipType) return false;
const efficiencyBonus = (skillState.skillUpgrades?.['efficientEnchant'] || []).length * 0.05 || 0; const totalCapacityCost = CraftingDesign.calculateDesignCapacityCost(effects, 0);
const totalCapacityCost = CraftingDesign.calculateDesignCapacityCost(effects, efficiencyBonus);
if (totalCapacityCost > equipType.baseCapacity) return false; if (totalCapacityCost > equipType.baseCapacity) return false;
const computedEffects = computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {});
const hasEnchantMastery = hasSpecial(computedEffects, SPECIAL_EFFECTS.ENCHANT_MASTERY);
let updates: Partial<CraftingState> = {}; let updates: Partial<CraftingState> = {};
if (!state.designProgress) { if (!state.designProgress) {
@@ -176,17 +155,6 @@ export const useCraftingStore = create<CraftingStore>()(
}; };
// Update currentAction in combatStore // Update currentAction in combatStore
useCombatStore.setState({ currentAction: 'design' }); useCombatStore.setState({ currentAction: 'design' });
} else if (hasEnchantMastery && !state.designProgress2) {
updates = {
designProgress2: {
designId: CraftingUtils.generateDesignId(),
progress: 0,
required: CraftingDesign.calculateDesignTime(effects),
name,
equipmentType: equipmentTypeId,
effects,
},
};
} else { } else {
return false; return false;
} }
+8 -20
View File
@@ -1,9 +1,7 @@
import { computeMaxMana } from '../utils'; import { computeMaxMana } from '../utils';
import { computeEffects } from '../upgrade-effects';
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { useManaStore } from './manaStore'; import { useManaStore } from './manaStore';
import { useSkillStore } from './skillStore';
import { useCombatStore } from './combatStore'; import { useCombatStore } from './combatStore';
export const createResetGame = (set: (state: any) => void, initialState: any) => () => { export const createResetGame = (set: (state: any) => void, initialState: any) => () => {
@@ -12,7 +10,6 @@ export const createResetGame = (set: (state: any) => void, initialState: any) =>
localStorage.removeItem('mana-loop-ui-storage'); localStorage.removeItem('mana-loop-ui-storage');
localStorage.removeItem('mana-loop-prestige-storage'); localStorage.removeItem('mana-loop-prestige-storage');
localStorage.removeItem('mana-loop-mana-storage'); localStorage.removeItem('mana-loop-mana-storage');
localStorage.removeItem('mana-loop-skill-storage');
localStorage.removeItem('mana-loop-combat-storage'); localStorage.removeItem('mana-loop-combat-storage');
localStorage.removeItem('mana-loop-game-storage'); localStorage.removeItem('mana-loop-game-storage');
localStorage.removeItem('mana-loop-crafting-storage'); localStorage.removeItem('mana-loop-crafting-storage');
@@ -24,7 +21,6 @@ export const createResetGame = (set: (state: any) => void, initialState: any) =>
useUIStore.getState().resetUI(); useUIStore.getState().resetUI();
usePrestigeStore.getState().resetPrestige(); usePrestigeStore.getState().resetPrestige();
useManaStore.getState().resetMana({}, {}, {}, {}); useManaStore.getState().resetMana({}, {}, {}, {});
useSkillStore.getState().resetSkills();
useCombatStore.getState().resetCombat(startFloor); useCombatStore.getState().resetCombat(startFloor);
set({ set({
@@ -34,28 +30,20 @@ export const createResetGame = (set: (state: any) => void, initialState: any) =>
}; };
export const createGatherMana = () => () => { export const createGatherMana = () => () => {
const skillState = useSkillStore.getState();
const manaState = useManaStore.getState(); const manaState = useManaStore.getState();
const prestigeState = usePrestigeStore.getState(); const prestigeState = usePrestigeStore.getState();
// Compute click mana // Base click mana (no skill bonuses)
let cm = 1 + const cm = 1;
(skillState.skills.manaTap || 0) * 1 +
(skillState.skills.manaSurge || 0) * 3;
// Mana overflow bonus
const overflowBonus = 1 + (skillState.skills.manaOverflow || 0) * 0.25;
cm = Math.floor(cm * overflowBonus);
const effects = computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {});
const max = computeMaxMana( const max = computeMaxMana(
{ {
skills: skillState.skills, skills: {},
prestigeUpgrades: prestigeState.prestigeUpgrades, prestigeUpgrades: prestigeState.prestigeUpgrades,
skillUpgrades: skillState.skillUpgrades, skillUpgrades: {},
skillTiers: skillState.skillTiers skillTiers: {}
}, },
effects undefined
); );
useManaStore.getState().gatherMana(cm, max); useManaStore.getState().gatherMana(cm, max);
+11 -24
View File
@@ -1,7 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useGameStore } from './gameStore'; import { useGameStore } from './gameStore';
import { useManaStore } from './manaStore'; import { useManaStore } from './manaStore';
import { useSkillStore } from './skillStore';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { useCombatStore } from './combatStore'; import { useCombatStore } from './combatStore';
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
@@ -28,17 +27,15 @@ export function useGameLoop() {
// ─── Shared Selector Hooks for Common Derived State ──────────────────────────── // ─── Shared Selector Hooks for Common Derived State ────────────────────────────
/** /**
* Get unified effects from all relevant stores * Get unified effects from equipment only (skills removed)
*/ */
export function useUnifiedEffects() { export function useUnifiedEffects() {
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const skillTiers = useSkillStore((s) => s.skillTiers);
const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
return getUnifiedEffects({ return getUnifiedEffects({
skillUpgrades, skillUpgrades: {},
skillTiers, skillTiers: {},
equippedInstances, equippedInstances,
equipmentInstances, equipmentInstances,
}); });
@@ -48,10 +45,7 @@ export function useUnifiedEffects() {
* Get computed mana stats (maxMana, baseRegen, clickMana, meditationMultiplier, effectiveRegen) * Get computed mana stats (maxMana, baseRegen, clickMana, meditationMultiplier, effectiveRegen)
*/ */
export function useManaStats() { export function useManaStats() {
const skills = useSkillStore((s) => s.skills);
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades); const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const skillTiers = useSkillStore((s) => s.skillTiers);
const meditateTicks = useManaStore((s) => s.meditateTicks); const meditateTicks = useManaStore((s) => s.meditateTicks);
const day = useGameStore((s) => s.day); const day = useGameStore((s) => s.day);
const hour = useGameStore((s) => s.hour); const hour = useGameStore((s) => s.hour);
@@ -59,30 +53,27 @@ export function useManaStats() {
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const upgradeEffects = getUnifiedEffects({ const upgradeEffects = getUnifiedEffects({
skillUpgrades, skillUpgrades: {},
skillTiers, skillTiers: {},
equippedInstances, equippedInstances,
equipmentInstances, equipmentInstances,
}); });
const maxMana = computeMaxMana( const maxMana = computeMaxMana(
{ skills, prestigeUpgrades, skillUpgrades, skillTiers }, { skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
upgradeEffects upgradeEffects
); );
const baseRegen = computeRegen( const baseRegen = computeRegen(
{ skills, prestigeUpgrades, skillUpgrades, skillTiers }, { skills: {}, prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
upgradeEffects upgradeEffects
); );
const clickMana = computeClickMana({ const clickMana = computeClickMana({
skills, skills: {},
prestigeUpgrades,
skillUpgrades,
skillTiers,
}); });
const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency); const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(day, hour); const incursionStrength = getIncursionStrength(day, hour);
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
@@ -115,22 +106,18 @@ export function useManaStats() {
* Get combat-related derived state * Get combat-related derived state
*/ */
export function useCombatStats() { export function useCombatStats() {
const skills = useSkillStore((s) => s.skills);
const signedPacts = usePrestigeStore((s) => s.signedPacts); const signedPacts = usePrestigeStore((s) => s.signedPacts);
const equippedInstances = useCraftingStore((s) => s.equippedInstances); const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances); const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
const skillTiers = useSkillStore((s) => s.skillTiers);
const upgradeEffects = getUnifiedEffects({ const upgradeEffects = getUnifiedEffects({
skillUpgrades, skillUpgrades: {},
skillTiers, skillTiers: {},
equippedInstances, equippedInstances,
equipmentInstances, equipmentInstances,
}); });
return { return {
skills,
signedPacts, signedPacts,
equippedInstances, equippedInstances,
equipmentInstances, equipmentInstances,
+2 -25
View File
@@ -4,21 +4,19 @@ import { SPELLS_DEF } from '../constants';
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { useManaStore } from './manaStore'; import { useManaStore } from './manaStore';
import { useSkillStore } from './skillStore';
import { useCombatStore } from './combatStore'; import { useCombatStore } from './combatStore';
export const createStartNewLoop = (set: (state: any) => void) => () => { export const createStartNewLoop = (set: (state: any) => void) => () => {
const prestigeState = usePrestigeStore.getState(); const prestigeState = usePrestigeStore.getState();
const combatState = useCombatStore.getState(); const combatState = useCombatStore.getState();
const manaState = useManaStore.getState(); const manaState = useManaStore.getState();
const skillState = useSkillStore.getState();
const insightGained = prestigeState.loopInsight || calcInsight({ const insightGained = prestigeState.loopInsight || calcInsight({
maxFloorReached: combatState.maxFloorReached, maxFloorReached: combatState.maxFloorReached,
totalManaGathered: manaState.totalManaGathered, totalManaGathered: manaState.totalManaGathered,
signedPacts: prestigeState.signedPacts, signedPacts: prestigeState.signedPacts,
prestigeUpgrades: prestigeState.prestigeUpgrades, prestigeUpgrades: prestigeState.prestigeUpgrades,
skills: skillState.skills, skills: {},
}); });
const total = prestigeState.insight + insightGained; const total = prestigeState.insight + insightGained;
@@ -26,25 +24,6 @@ export const createStartNewLoop = (set: (state: any) => void) => () => {
const pu = prestigeState.prestigeUpgrades; const pu = prestigeState.prestigeUpgrades;
const startFloor = 1 + (pu.spireKey || 0) * 2; const startFloor = 1 + (pu.spireKey || 0) * 2;
// Apply saved memories - restore skill levels, tiers, and upgrades
const memories = prestigeState.memories || [];
const newSkills: Record<string, number> = {};
const newSkillTiers: Record<string, number> = {};
const newSkillUpgrades: Record<string, string[]> = {};
if (memories.length > 0) {
for (const memory of memories) {
const tieredSkillId = memory.tier > 1 ? `${memory.skillId}_t${memory.tier}` : memory.skillId;
newSkills[tieredSkillId] = memory.level;
if (memory.tier > 1) {
newSkillTiers[memory.skillId] = memory.tier;
}
newSkillUpgrades[tieredSkillId] = memory.upgrades || [];
}
}
// Reset and update all stores for new loop // Reset and update all stores for new loop
useUIStore.setState({ useUIStore.setState({
gameOver: false, gameOver: false,
@@ -61,9 +40,7 @@ export const createStartNewLoop = (set: (state: any) => void) => () => {
); );
usePrestigeStore.getState().incrementLoopCount(); usePrestigeStore.getState().incrementLoopCount();
useManaStore.getState().resetMana(pu, newSkills, newSkillUpgrades, newSkillTiers); useManaStore.getState().resetMana(pu, {}, {}, {});
useSkillStore.getState().resetSkills(newSkills, newSkillUpgrades, newSkillTiers);
// Reset combat with starting floor and any spells from prestige upgrades // Reset combat with starting floor and any spells from prestige upgrades
const startSpells = makeInitialSpells(); const startSpells = makeInitialSpells();
+20 -59
View File
@@ -5,7 +5,6 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { TICK_MS, HOURS_PER_TICK, MAX_DAY, SPELLS_DEF, GUARDIANS, getStudySpeedMultiplier } from '../constants'; import { TICK_MS, HOURS_PER_TICK, MAX_DAY, SPELLS_DEF, GUARDIANS, getStudySpeedMultiplier } from '../constants';
import { computeEffects } from '../upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects'; import { hasSpecial, SPECIAL_EFFECTS } from '../special-effects';
import { import {
computeMaxMana, computeMaxMana,
@@ -22,7 +21,6 @@ import {
import { useUIStore } from './uiStore'; import { useUIStore } from './uiStore';
import { usePrestigeStore } from './prestigeStore'; import { usePrestigeStore } from './prestigeStore';
import { useManaStore } from './manaStore'; import { useManaStore } from './manaStore';
import { useSkillStore } from './skillStore';
import { useCombatStore, makeInitialSpells } from './combatStore'; import { useCombatStore, makeInitialSpells } from './combatStore';
import { useAttunementStore } from './attunementStore'; import { useAttunementStore } from './attunementStore';
import { ATTUNEMENTS_DEF, getAttunementConversionRate } from '../data/attunements'; import { ATTUNEMENTS_DEF, getAttunementConversionRate } from '../data/attunements';
@@ -66,26 +64,22 @@ export const useGameStore = create<GameCoordinatorStore>()(
tick: () => { tick: () => {
const uiState = useUIStore.getState(); const uiState = useUIStore.getState();
if (uiState.gameOver || uiState.paused) return; if (uiState.gameOver || uiState.paused) return;
// Helper for logging // Helper for logging
const addLog = (msg: string) => useUIStore.getState().addLog(msg); const addLog = (msg: string) => useUIStore.getState().addLog(msg);
// Get all store states // Get all store states
const prestigeState = usePrestigeStore.getState(); const prestigeState = usePrestigeStore.getState();
const manaState = useManaStore.getState(); const manaState = useManaStore.getState();
const skillState = useSkillStore.getState();
const combatState = useCombatStore.getState(); const combatState = useCombatStore.getState();
// Compute effects from upgrades
const effects = computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {});
const maxMana = computeMaxMana( const maxMana = computeMaxMana(
{ skills: skillState.skills, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: skillState.skillUpgrades, skillTiers: skillState.skillTiers }, { skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {} },
effects undefined
); );
const baseRegen = computeRegen( const baseRegen = computeRegen(
{ skills: skillState.skills, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: skillState.skillUpgrades, skillTiers: skillState.skillTiers }, { skills: {}, prestigeUpgrades: prestigeState.prestigeUpgrades, skillUpgrades: {}, skillTiers: {}, attunement: {} },
effects undefined
); );
// Time progression // Time progression
@@ -103,9 +97,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
totalManaGathered: manaState.totalManaGathered, totalManaGathered: manaState.totalManaGathered,
signedPacts: prestigeState.signedPacts, signedPacts: prestigeState.signedPacts,
prestigeUpgrades: prestigeState.prestigeUpgrades, prestigeUpgrades: prestigeState.prestigeUpgrades,
skills: skillState.skills, skills: {},
}); });
addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`); addLog(`⏰ The loop ends. Gained ${insightGained} Insight.`);
useUIStore.getState().setGameOver(true, false); useUIStore.getState().setGameOver(true, false);
usePrestigeStore.getState().setLoopInsight(insightGained); usePrestigeStore.getState().setLoopInsight(insightGained);
@@ -120,9 +114,9 @@ export const useGameStore = create<GameCoordinatorStore>()(
totalManaGathered: manaState.totalManaGathered, totalManaGathered: manaState.totalManaGathered,
signedPacts: prestigeState.signedPacts, signedPacts: prestigeState.signedPacts,
prestigeUpgrades: prestigeState.prestigeUpgrades, prestigeUpgrades: prestigeState.prestigeUpgrades,
skills: skillState.skills, skills: {},
}) * 3; }) * 3;
addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`); addLog(`🏆 VICTORY! The Awakened One falls! Gained ${insightGained} Insight!`);
useUIStore.getState().setGameOver(true, true); useUIStore.getState().setGameOver(true, true);
usePrestigeStore.getState().setLoopInsight(insightGained); usePrestigeStore.getState().setLoopInsight(insightGained);
@@ -135,10 +129,10 @@ export const useGameStore = create<GameCoordinatorStore>()(
// Meditation bonus tracking and regen calculation // Meditation bonus tracking and regen calculation
let meditateTicks = manaState.meditateTicks; let meditateTicks = manaState.meditateTicks;
let meditationMultiplier = 1; let meditationMultiplier = 1;
if (combatState.currentAction === 'meditate') { if (combatState.currentAction === 'meditate') {
meditateTicks++; meditateTicks++;
meditationMultiplier = getMeditationBonus(meditateTicks, skillState.skills, effects.meditationEfficiency); meditationMultiplier = getMeditationBonus(meditateTicks, {}, 1);
} else { } else {
meditateTicks = 0; meditateTicks = 0;
} }
@@ -150,7 +144,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
if (!state.active) return; if (!state.active) return;
const def = ATTUNEMENTS_DEF[id]; const def = ATTUNEMENTS_DEF[id];
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return; if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
const scaledRate = getAttunementConversionRate(id, state.level || 1); const scaledRate = getAttunementConversionRate(id, state.level || 1);
totalConversionPerTick += scaledRate * HOURS_PER_TICK; totalConversionPerTick += scaledRate * HOURS_PER_TICK;
}); });
@@ -167,10 +161,10 @@ export const useGameStore = create<GameCoordinatorStore>()(
if (!state.active) return; if (!state.active) return;
const def = ATTUNEMENTS_DEF[id]; const def = ATTUNEMENTS_DEF[id];
if (!def || def.conversionRate <= 0 || !def.primaryManaType) return; if (!def || def.conversionRate <= 0 || !def.primaryManaType) return;
const scaledRate = getAttunementConversionRate(id, state.level || 1); const scaledRate = getAttunementConversionRate(id, state.level || 1);
const conversionThisTick = scaledRate * HOURS_PER_TICK; // per tick const conversionThisTick = scaledRate * HOURS_PER_TICK;
// Add to primary mana type (cost already deducted from regen) // Add to primary mana type (cost already deducted from regen)
if (elements[def.primaryManaType]) { if (elements[def.primaryManaType]) {
elements[def.primaryManaType].current = Math.min( elements[def.primaryManaType].current = Math.min(
@@ -181,32 +175,6 @@ export const useGameStore = create<GameCoordinatorStore>()(
}); });
let totalManaGathered = manaState.totalManaGathered; let totalManaGathered = manaState.totalManaGathered;
// Study progress - handled by skillStore
if (combatState.currentAction === 'study' && skillState.currentStudyTarget) {
const studySpeedMult = getStudySpeedMultiplier(skillState.skills);
const progressGain = HOURS_PER_TICK * studySpeedMult;
const result = useSkillStore.getState().updateStudyProgress(progressGain);
if (result.completed && result.target) {
if (result.target.type === 'skill') {
const skillId = result.target.id;
const currentLevel = skillState.skills[skillId] || 0;
// Update skill level
useSkillStore.getState().incrementSkillLevel(skillId);
useSkillStore.getState().clearPaidStudySkill(skillId);
useCombatStore.getState().setAction('meditate');
addLog(`${skillId} Lv.${currentLevel + 1} mastered!`);
} else if (result.target.type === 'spell') {
const spellId = result.target.id;
useCombatStore.getState().learnSpell(spellId);
useSkillStore.getState().setCurrentStudyTarget(null);
useCombatStore.getState().setAction('meditate');
addLog(`📖 ${SPELLS_DEF[spellId]?.name || spellId} learned!`);
}
}
}
// Convert action - delegate to manaStore // Convert action - delegate to manaStore
if (combatState.currentAction === 'convert') { if (combatState.currentAction === 'convert') {
const convertResult = useManaStore.getState().processConvertAction(rawMana); const convertResult = useManaStore.getState().processConvertAction(rawMana);
@@ -238,11 +206,11 @@ export const useGameStore = create<GameCoordinatorStore>()(
// Combat - delegate to combatStore // Combat - delegate to combatStore
if (combatState.currentAction === 'climb') { if (combatState.currentAction === 'climb') {
const combatResult = useCombatStore.getState().processCombatTick( const combatResult = useCombatStore.getState().processCombatTick(
skillState.skills, {},
rawMana, rawMana,
elements, elements,
maxMana, maxMana,
effects.attackSpeedMultiplier, 1,
(floor, wasGuardian) => { (floor, wasGuardian) => {
if (wasGuardian) { if (wasGuardian) {
addLog(`⚔️ ${GUARDIANS[floor]?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`); addLog(`⚔️ ${GUARDIANS[floor]?.name || 'Guardian'} defeated! Visit the Grimoire to sign a pact.`);
@@ -252,25 +220,18 @@ export const useGameStore = create<GameCoordinatorStore>()(
}, },
(damage) => { (damage) => {
// Apply upgrade damage multipliers and bonuses // Apply upgrade damage multipliers and bonuses
let dmg = damage * effects.baseDamageMultiplier + effects.baseDamageBonus; let dmg = damage;
// Executioner: +100% damage to enemies below 25% HP // Executioner: +100% damage to enemies below 25% HP
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && combatState.floorHP / combatState.floorMaxHP < 0.25) { if (hasSpecial({}, SPECIAL_EFFECTS.EXECUTIONER) && combatState.floorHP / combatState.floorMaxHP < 0.25) {
dmg *= 2; dmg *= 2;
} }
// Berserker: +50% damage when below 50% mana // Berserker: +50% damage when below 50% mana
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) { if (hasSpecial({}, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
dmg *= 1.5; dmg *= 1.5;
} }
// Spell echo - chance to cast again
const echoChance = (skillState.skills.spellEcho || 0) * 0.1;
if (Math.random() < echoChance) {
dmg *= 2;
addLog(`✨ Spell Echo! Double damage!`);
}
return { rawMana, elements, modifiedDamage: dmg }; return { rawMana, elements, modifiedDamage: dmg };
} }
); );
-3
View File
@@ -11,9 +11,6 @@ export type { PrestigeState } from './prestigeStore';
export { useManaStore, makeInitialElements } from './manaStore'; export { useManaStore, makeInitialElements } from './manaStore';
export type { ManaState } from './manaStore'; export type { ManaState } from './manaStore';
export { useSkillStore } from './skillStore';
export type { SkillState } from './skillStore';
export { useCombatStore, makeInitialSpells } from './combatStore'; export { useCombatStore, makeInitialSpells } from './combatStore';
export type { CombatState } from './combatStore'; export type { CombatState } from './combatStore';
-211
View File
@@ -1,211 +0,0 @@
// ─── Study Slice ─────────────────────────────────────────────────────────────
// Actions for studying skills and spells
import type { GameState } from './types';
import { SKILLS_DEF, SPELLS_DEF, getStudyCostMultiplier } from './constants';
import { computeEffects } from './upgrade-effects';
import { hasSpecial, SPECIAL_EFFECTS } from './special-effects';
// ─── Study Actions Interface ──────────────────────────────────────────────────
export interface StudyActions {
startStudyingSkill: (skillId: string) => void;
startStudyingSpell: (spellId: string) => void;
cancelStudy: () => void;
startParallelStudySkill: (skillId: string) => void;
cancelParallelStudy: () => void;
}
// ─── Study Slice Factory ──────────────────────────────────────────────────────
export function createStudySlice(
set: (partial: Partial<GameState> | ((state: GameState) => Partial<GameState>)) => void,
get: () => GameState
): StudyActions {
return {
// Start studying a skill - mana is deducted per hour, not upfront
startStudyingSkill: (skillId: string) => {
const state = get();
const sk = SKILLS_DEF[skillId];
if (!sk) return;
const currentLevel = state.skills[skillId] || 0;
if (currentLevel >= sk.max) return;
// Check prerequisites
if (sk.req) {
for (const [r, rl] of Object.entries(sk.req)) {
if ((state.skills[r] || 0) < rl) return;
}
}
// Calculate total mana cost and cost per hour
const costMult = getStudyCostMultiplier(state.skills);
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: initialProgress,
required: sk.studyTime,
manaCostPerHour: manaCostPerHour,
totalCost: totalCost,
},
log: [`📚 Started studying ${sk.name} (${manaCostPerHour} mana/hr)...`, ...state.log.slice(0, 49)],
});
},
// Start studying a spell
startStudyingSpell: (spellId: string) => {
const state = get();
const sp = SPELLS_DEF[spellId];
if (!sp || state.spells[spellId]?.learned) return;
// Calculate total mana cost and cost per hour
const costMult = getStudyCostMultiplier(state.skills);
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);
// Must have at least 1 hour worth of mana to start
if (state.rawMana < manaCostPerHour) return;
// Start studying (no upfront cost - mana is deducted per hour during study)
set({
currentAction: 'study',
currentStudyTarget: {
type: 'spell',
id: spellId,
progress: state.spells[spellId]?.studyProgress || 0,
required: studyTime,
manaCostPerHour: manaCostPerHour,
totalCost: totalCost,
},
spells: {
...state.spells,
[spellId]: { ...(state.spells[spellId] || { learned: false, level: 0 }), studyProgress: state.spells[spellId]?.studyProgress || 0 },
},
log: [`📚 Started studying ${sp.name} (${manaCostPerHour} mana/hr)...`, ...state.log.slice(0, 49)],
});
},
// Cancel current study (saves progress)
cancelStudy: () => {
const state = get();
if (!state.currentStudyTarget) return;
// Knowledge retention bonus
const retentionBonus = 1 + (state.skills.knowledgeRetention || 0) * 0.2;
const savedProgress = Math.min(
state.currentStudyTarget.progress,
state.currentStudyTarget.required * retentionBonus
);
// Save progress
if (state.currentStudyTarget.type === 'skill') {
set({
currentStudyTarget: null,
currentAction: 'meditate',
skillProgress: {
...state.skillProgress,
[state.currentStudyTarget.id]: savedProgress,
},
log: [`📖 Study interrupted. Progress saved.`, ...state.log.slice(0, 49)],
});
} else if (state.currentStudyTarget.type === 'spell') {
set({
currentStudyTarget: null,
currentAction: 'meditate',
spells: {
...state.spells,
[state.currentStudyTarget.id]: {
...(state.spells[state.currentStudyTarget.id] || { learned: false, level: 0 }),
studyProgress: savedProgress,
},
},
log: [`📖 Study interrupted. Progress saved.`, ...state.log.slice(0, 49)],
});
}
},
// Start parallel study of a skill (requires Parallel Mind upgrade)
startParallelStudySkill: (skillId: string) => {
const state = get();
if (state.parallelStudyTarget) return; // Already have parallel study
if (!state.currentStudyTarget) return; // Need primary study
const sk = SKILLS_DEF[skillId];
if (!sk) return;
const currentLevel = state.skills[skillId] || 0;
if (currentLevel >= sk.max) return;
// Can't study same thing in parallel
if (state.currentStudyTarget.id === skillId) return;
// Calculate mana cost for parallel study
const costMult = getStudyCostMultiplier(state.skills);
const totalCost = Math.floor(sk.base * (currentLevel + 1) * costMult);
const manaCostPerHour = Math.ceil(totalCost / sk.studyTime);
set({
parallelStudyTarget: {
type: 'skill',
id: skillId,
progress: state.skillProgress[skillId] || 0,
required: sk.studyTime,
manaCostPerHour: Math.ceil(manaCostPerHour / 2), // Half speed = half mana cost per tick
totalCost: totalCost,
},
log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)],
});
},
// Cancel parallel study
cancelParallelStudy: () => {
set((state) => {
if (!state.parallelStudyTarget) return state;
return {
parallelStudyTarget: null,
log: ['📖 Parallel study cancelled.', ...state.log.slice(0, 49)],
};
});
},
};
}
+26 -57
View File
@@ -11,67 +11,36 @@ export type { AttunementSlot, AttunementDef, AttunementState, GuardianBoon, Guar
// Spell types // Spell types
export type { SpellCost, SpellDef, SpellEffect, SpellState } from './spells'; export type { SpellCost, SpellDef, SpellEffect, SpellState } from './spells';
// Skill types
export type {
SkillDef,
SkillUpgradeDef,
SkillUpgradeEffect,
SkillEvolutionPath,
SkillTierDef,
SkillPerkChoice,
SkillUpgradeChoice,
PrestigeDef,
SkillCost
} from './skills';
// Equipment types // Equipment types
export type { export type {
EquipmentDef, EquipmentDef,
EquipmentInstance, EquipmentInstance,
AppliedEnchantment, AppliedEnchantment,
EnchantmentDesign, EnchantmentDesign,
DesignEffect, DesignEffect,
DesignProgress, DesignProgress,
PreparationProgress, PreparationProgress,
ApplicationProgress, ApplicationProgress,
EquipmentCraftingProgress, EquipmentCraftingProgress,
EquipmentSpellState, EquipmentSpellState,
BlueprintDef, BlueprintDef,
LootInventory, LootInventory,
EquipmentSlot EquipmentSlot
} from './equipmentSlot'; } from './equipmentSlot';
// Game state types // Game state types
export type { export type {
RoomType, RoomType,
EnemyState, EnemyState,
FloorState, FloorState,
AchievementDef, AchievementDef,
AchievementState, AchievementState,
GameAction, GameAction,
ScheduleBlock, ScheduleBlock,
StudyTarget, StudyTarget,
SummonedGolem, SummonedGolem,
GolemancyState, GolemancyState,
GameState, GameState,
GameActionType,
ActivityEventType,
ActivityLogEntry,
} from './game';
// Game state types
export type {
RoomType,
EnemyState,
FloorState,
AchievementDef,
AchievementState,
GameAction,
ScheduleBlock,
StudyTarget,
SummonedGolem,
GolemancyState,
GameState,
GameActionType, GameActionType,
ActivityEventType, ActivityEventType,
ActivityLogEntry, ActivityLogEntry,
+41
View File
@@ -1,5 +1,8 @@
// ─── Formatting Functions ───────────────────────────────────────────────────── // ─── Formatting Functions ─────────────────────────────────────────────────────
import { ELEMENTS } from '@/lib/game/constants';
import type { SpellCost } from '@/lib/game/types';
export function fmt(n: number): string { export function fmt(n: number): string {
if (!isFinite(n) || isNaN(n)) return '0'; if (!isFinite(n) || isNaN(n)) return '0';
if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B'; if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B';
@@ -11,3 +14,41 @@ export function fmt(n: number): string {
export function fmtDec(n: number, d: number = 1): string { export function fmtDec(n: number, d: number = 1): string {
return isFinite(n) ? n.toFixed(d) : '0'; return isFinite(n) ? n.toFixed(d) : '0';
} }
/**
* Format a spell cost for display
*/
export function formatSpellCost(cost: SpellCost): string {
if (cost.type === 'raw') {
return `${cost.amount} raw`;
}
const elemDef = ELEMENTS[cost.element || ''];
return `${cost.amount} ${elemDef?.sym || '?'}`;
}
/**
* Get the display color for a spell cost
*/
export function getSpellCostColor(cost: SpellCost): string {
if (cost.type === 'raw') {
return '#60A5FA'; // Blue for raw mana
}
return ELEMENTS[cost.element || '']?.color || '#9CA3AF';
}
/**
* Format study time in hours to human-readable string
*/
export function formatStudyTime(hours: number): string {
if (hours < 1) return `${Math.round(hours * 60)}m`;
return `${hours.toFixed(1)}h`;
}
/**
* Format time (hour of day) to HH:MM format
*/
export function formatHour(hour: number): string {
const h = Math.floor(hour);
const m = Math.floor((hour % 1) * 60);
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
}
+1 -1
View File
@@ -1,7 +1,7 @@
// ─── Game Utilities - Barrel Export ────────────────────────────────────────── // ─── Game Utilities - Barrel Export ──────────────────────────────────────────
// Re-export everything from the focused modules // Re-export everything from the focused modules
export { fmt, fmtDec } from './formatting'; export { fmt, fmtDec, formatSpellCost, getSpellCostColor, formatStudyTime, formatHour } from './formatting';
export { getFloorMaxHP, getFloorElement } from './floor-utils'; export { getFloorMaxHP, getFloorElement } from './floor-utils';
export { export {
computeMaxMana, computeMaxMana,