cleanup: delete computed-stats.ts shim and store/index.ts
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 57s
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:
@@ -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,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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
+17
-41
@@ -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 />}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>,
|
||||||
@@ -62,24 +59,6 @@ function createUnifiedStore(
|
|||||||
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,
|
||||||
@@ -131,7 +110,6 @@ 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();
|
||||||
@@ -139,27 +117,24 @@ export function GameProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
// 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(
|
||||||
@@ -182,8 +157,8 @@ export function GameProvider({ children }: { children: ReactNode }) {
|
|||||||
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(
|
||||||
@@ -191,15 +166,9 @@ export function GameProvider({ children }: { children: ReactNode }) {
|
|||||||
[gameStore.day, gameStore.hour]
|
[gameStore.day, gameStore.hour]
|
||||||
);
|
);
|
||||||
|
|
||||||
const studySpeedMult = useMemo(
|
const studySpeedMult = 1;
|
||||||
() => getStudySpeedMultiplier(skillState.skills),
|
|
||||||
[skillState.skills]
|
|
||||||
);
|
|
||||||
|
|
||||||
const studyCostMult = useMemo(
|
const studyCostMult = 1;
|
||||||
() => getStudyCostMultiplier(skillState.skills),
|
|
||||||
[skillState.skills]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Effective regen calculations
|
// Effective regen calculations
|
||||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||||
@@ -230,15 +199,15 @@ export function GameProvider({ children }: { children: ReactNode }) {
|
|||||||
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 => {
|
||||||
@@ -249,7 +218,6 @@ export function GameProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const value: GameContextValue = {
|
const value: GameContextValue = {
|
||||||
store: unifiedStore,
|
store: unifiedStore,
|
||||||
skillStore: skillState,
|
|
||||||
manaStore: manaState,
|
manaStore: manaState,
|
||||||
prestigeStore: prestigeState,
|
prestigeStore: prestigeState,
|
||||||
uiStore: uiState,
|
uiStore: uiState,
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -33,33 +32,6 @@ export interface UnifiedStore {
|
|||||||
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;
|
||||||
@@ -112,7 +84,6 @@ export interface GameContextValue {
|
|||||||
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>;
|
||||||
|
|||||||
@@ -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,21 +24,12 @@ 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];
|
||||||
|
|
||||||
@@ -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">
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,6 +34,12 @@ 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" />
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ 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);
|
||||||
@@ -17,7 +16,6 @@ export function LoopStatsSection() {
|
|||||||
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)]">
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,25 +25,17 @@ 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>
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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";
|
|
||||||
@@ -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";
|
|
||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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';
|
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
@@ -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 || {}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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')}`;
|
|
||||||
}
|
|
||||||
@@ -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)],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -5,31 +5,17 @@
|
|||||||
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) {
|
||||||
@@ -58,7 +44,7 @@ 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) {
|
||||||
@@ -86,8 +72,7 @@ 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 || {});
|
||||||
@@ -127,8 +112,7 @@ 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);
|
||||||
@@ -156,12 +140,10 @@ 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;
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
|
||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ export interface CombatState {
|
|||||||
|
|
||||||
// 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,
|
||||||
@@ -325,7 +324,6 @@ export const useCombatStore = create<CombatState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
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,
|
||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -73,19 +71,15 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
// 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,7 +97,7 @@ 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.`);
|
||||||
@@ -120,7 +114,7 @@ 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!`);
|
||||||
@@ -138,7 +132,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -169,7 +163,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
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]) {
|
||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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)],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -11,19 +11,6 @@ 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,
|
||||||
@@ -58,21 +45,3 @@ export type {
|
|||||||
ActivityEventType,
|
ActivityEventType,
|
||||||
ActivityLogEntry,
|
ActivityLogEntry,
|
||||||
} from './game';
|
} from './game';
|
||||||
|
|
||||||
// Game state types
|
|
||||||
export type {
|
|
||||||
RoomType,
|
|
||||||
EnemyState,
|
|
||||||
FloorState,
|
|
||||||
AchievementDef,
|
|
||||||
AchievementState,
|
|
||||||
GameAction,
|
|
||||||
ScheduleBlock,
|
|
||||||
StudyTarget,
|
|
||||||
SummonedGolem,
|
|
||||||
GolemancyState,
|
|
||||||
GameState,
|
|
||||||
GameActionType,
|
|
||||||
ActivityEventType,
|
|
||||||
ActivityLogEntry,
|
|
||||||
} from './game';
|
|
||||||
|
|||||||
@@ -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,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,
|
||||||
|
|||||||
Reference in New Issue
Block a user