diff --git a/src/app/page.tsx b/src/app/page.tsx index cb75679..09cd482 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useGameStore, useGameLoop, fmt, fmtDec, getFloorElement, computeMaxMana, computeRegen, computeClickMana, calcDamage, getMeditationBonus, getIncursionStrength, canAffordSpellCost } from '@/lib/game/store'; -import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, MAX_DAY, INCURSION_START_DAY, MANA_PER_ELEMENT, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier, ELEMENT_OPPOSITES, HOURS_PER_TICK, TICK_MS } from '@/lib/game/constants'; +import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, PRESTIGE_DEF, SKILL_CATEGORIES, getStudySpeedMultiplier, getStudyCostMultiplier, ELEMENT_OPPOSITES, HOURS_PER_TICK, TICK_MS } from '@/lib/game/constants'; import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; import { LOOT_DROPS } from '@/lib/game/data/loot-drops'; @@ -21,9 +21,9 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Sparkles, Swords, BookOpen, FlaskConical, Trophy, RotateCcw, Pause, Play, Zap, Clock, Target, Flame, Droplet, Wind, Mountain, Sun, Moon, Leaf, Skull, Brain, Link, Wind as Force, Droplets, TreeDeciduous, Hourglass, Gem, Star, CircleDot, X, Circle, BarChart3, Award, Package, Heart, ChevronUp, ChevronDown } from 'lucide-react'; import type { GameAction } from '@/lib/game/types'; -import { CraftingTab } from '@/components/game/tabs/CraftingTab'; +import { CraftingTab, SpireTab, SpellsTab, LabTab } from '@/components/game/tabs'; import { FamiliarTab } from '@/components/game/tabs/FamiliarTab'; -import { ComboMeter } from '@/components/game/ComboMeter'; +import { ComboMeter, ActionButtons, CalendarDisplay, CraftingProgress, StudyProgress, ManaDisplay, TimeDisplay } from '@/components/game'; import { LootInventoryDisplay } from '@/components/game/LootInventory'; import { AchievementsDisplay } from '@/components/game/AchievementsDisplay'; import { ACHIEVEMENTS } from '@/lib/game/data/achievements'; @@ -237,279 +237,6 @@ export default function ManaLoopGame() { // Format time (use shared utility) const formatTime = formatHour; - // Calendar rendering - const renderCalendar = () => { - const days: React.ReactElement[] = []; - for (let d = 1; d <= MAX_DAY; d++) { - let dayClass = 'w-7 h-7 rounded text-xs flex items-center justify-center font-mono border transition-all '; - - if (d < store.day) { - dayClass += 'bg-blue-900/30 border-blue-800/50 text-blue-400'; - } else if (d === store.day) { - dayClass += 'bg-blue-600/40 border-blue-500 text-blue-300 shadow-lg shadow-blue-500/30'; - } else { - dayClass += 'bg-gray-800/30 border-gray-700/50 text-gray-500'; - } - - if (d >= INCURSION_START_DAY) { - dayClass += ' border-red-600/50'; - } - - days.push( - - -
- {d} -
-
- -

Day {d}

- {d >= INCURSION_START_DAY &&

Incursion Active

} -
-
- ); - } - return days; - }; - - // Action buttons - const renderActionButtons = () => { - const actions: { id: GameAction; label: string; icon: typeof Swords; disabled?: boolean }[] = [ - { id: 'meditate', label: 'Meditate', icon: Sparkles }, - { id: 'climb', label: 'Climb', icon: Swords }, - { id: 'study', label: 'Study', icon: BookOpen }, - { id: 'convert', label: 'Convert', icon: FlaskConical }, - ]; - - // Add crafting actions if player has progress - const hasDesignProgress = store.designProgress !== null; - const hasPrepProgress = store.preparationProgress !== null; - const hasAppProgress = store.applicationProgress !== null; - - return ( -
-
- {actions.map(({ id, label, icon: Icon }) => ( - - ))} -
- - {/* Crafting actions row - shown when there's active crafting progress */} - {(hasDesignProgress || hasPrepProgress || hasAppProgress) && ( -
- - - -
- )} -
- ); - }; - - // Render crafting progress - const renderCraftingProgress = () => { - const progressSections: React.ReactNode[] = []; - - // Design progress - if (store.designProgress) { - const progressPct = Math.min(100, (store.designProgress.progress / store.designProgress.required) * 100); - progressSections.push( -
-
-
- - - Designing Enchantment - -
- -
- -
- {formatStudyTime(store.designProgress.progress)} / {formatStudyTime(store.designProgress.required)} - Design Time -
-
- ); - } - - // Preparation progress - if (store.preparationProgress) { - const progressPct = Math.min(100, (store.preparationProgress.progress / store.preparationProgress.required) * 100); - const instance = store.equipmentInstances[store.preparationProgress.equipmentInstanceId]; - progressSections.push( -
-
-
- - - Preparing {instance?.name || 'Equipment'} - -
- -
- -
- {formatStudyTime(store.preparationProgress.progress)} / {formatStudyTime(store.preparationProgress.required)} - Mana spent: {fmt(store.preparationProgress.manaCostPaid)} -
-
- ); - } - - // Application progress - if (store.applicationProgress) { - const progressPct = Math.min(100, (store.applicationProgress.progress / store.applicationProgress.required) * 100); - const instance = store.equipmentInstances[store.applicationProgress.equipmentInstanceId]; - const design = store.enchantmentDesigns.find(d => d.id === store.applicationProgress?.designId); - progressSections.push( -
-
-
- - - Enchanting {instance?.name || 'Equipment'} - -
-
- {store.applicationProgress.paused ? ( - - ) : ( - - )} - -
-
- -
- {formatStudyTime(store.applicationProgress.progress)} / {formatStudyTime(store.applicationProgress.required)} - Mana/hr: {fmt(store.applicationProgress.manaPerHour)} -
- {design && ( -
- Applying: {design.name} -
- )} -
- ); - } - - return progressSections.length > 0 ? ( -
- {progressSections} -
- ) : null; - }; - - // Render current study progress - const renderStudyProgress = () => { - if (!store.currentStudyTarget) return null; - - const target = store.currentStudyTarget; - 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; - - return ( -
-
-
- - - {def?.name} - {isSkill && ` Lv.${currentLevel + 1}`} - -
- -
- -
- {formatStudyTime(target.progress)} / {formatStudyTime(target.required)} - {studySpeedMult.toFixed(1)}x speed -
-
- ); - }; - // Check if spell can be cast const canCastSpell = (spellId: string): boolean => { const spell = SPELLS_DEF[spellId]; @@ -517,610 +244,8 @@ export default function ManaLoopGame() { return canAffordSpellCost(spell.cost, store.rawMana, store.elements); }; - // Spire Tab - const renderSpireTab = () => ( -
- {/* Current Floor Card */} - - - Current Floor - - -
- - {store.currentFloor} - - / 100 - - {floorElemDef?.sym} {floorElemDef?.name} - - {isGuardianFloor && ( - GUARDIAN - )} -
- - {isGuardianFloor && currentGuardian && ( -
- ⚔️ {currentGuardian.name} -
- )} - - {/* HP Bar */} -
-
-
-
-
- {fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP - DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'} -
-
- - - - {/* Floor Navigation Controls */} -
-
- Auto-Direction -
- - -
-
- -
- - -
- - {store.clearedFloors?.[store.currentFloor] && ( -
- - Floor will respawn when you leave and return -
- )} -
- - - -
- Best: Floor {store.maxFloorReached} • - Pacts: {store.signedPacts.length} -
- - - {/* Active Spells Card - Shows all spells from equipped weapons */} - - - - Active Spells ({activeEquipmentSpells.length}) - - - - {activeEquipmentSpells.length > 0 ? ( -
- {activeEquipmentSpells.map(({ spellId, equipmentId }) => { - const spellDef = SPELLS_DEF[spellId]; - if (!spellDef) return null; - - const spellState = store.equipmentSpellStates?.find( - s => s.spellId === spellId && s.sourceEquipment === equipmentId - ); - const progress = spellState?.castProgress || 0; - const canCast = canAffordSpellCost(spellDef.cost, store.rawMana, store.elements); - - return ( -
-
-
- {spellDef.name} - {spellDef.tier === 0 && Basic} - {spellDef.tier >= 4 && Legendary} -
- - {canCast ? '✓' : '✗'} - -
-
- ⚔️ {fmt(calcDamage(store, spellId, floorElem))} dmg • - - {' '}{formatSpellCost(spellDef.cost)} - - {' '}• ⚡ {(spellDef.castSpeed || 1).toFixed(1)}/hr -
- - {/* Cast progress bar when climbing */} - {store.currentAction === 'climb' && ( -
-
- Cast - {(progress * 100).toFixed(0)}% -
- -
- )} - - {spellDef.effects && spellDef.effects.length > 0 && ( -
- {spellDef.effects.map((eff, i) => ( - - {eff.type === 'lifesteal' && `🩸 ${Math.round(eff.value * 100)}%`} - {eff.type === 'burn' && `🔥 Burn`} - {eff.type === 'freeze' && `❄️ Freeze`} - - ))} -
- )} -
- ); - })} -
- ) : ( -
No spells on equipped weapons. Enchant a staff with spell effects.
- )} - - {incursionStrength > 0 && ( -
-
LABYRINTH INCURSION
-
- -{Math.round(incursionStrength * 100)}% mana regen -
-
- )} -
-
- {/* Combo Meter - Shows when climbing or has active combo */} - - - {/* Current Study (if any) */} - {store.currentStudyTarget && ( - - - {renderStudyProgress()} - - {/* Parallel Study Progress */} - {store.parallelStudyTarget && ( -
-
-
- - - Parallel: {SKILLS_DEF[store.parallelStudyTarget.id]?.name} - {store.parallelStudyTarget.type === 'skill' && ` Lv.${(store.skills[store.parallelStudyTarget.id] || 0) + 1}`} - -
- -
- -
- {formatStudyTime(store.parallelStudyTarget.progress)} / {formatStudyTime(store.parallelStudyTarget.required)} - 50% speed (Parallel Study) -
-
- )} -
-
- )} - - {/* Crafting Progress (if any) */} - {(store.designProgress || store.preparationProgress || store.applicationProgress) && ( - - - {renderCraftingProgress()} - - - )} - - {/* Spells Available */} - - - Known Spells - - -
- {Object.entries(store.spells) - .filter(([, state]) => state.learned) - .map(([id, state]) => { - const def = SPELLS_DEF[id]; - if (!def) return null; - const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem]; - const isActive = store.activeSpell === id; - const canCast = canCastSpell(id); - - return ( - - ); - })} -
-
-
- - {/* Activity Log */} - - - Activity Log - - - -
- {store.log.slice(0, 20).map((entry, i) => ( -
- {entry} -
- ))} -
-
-
-
-
- ); - - // Spells Tab - const renderSpellsTab = () => { - // Get spells from equipment - const equipmentSpellIds = store.getEquipmentSpells ? store.getEquipmentSpells() : []; - const equipmentSpells = equipmentSpellIds.map(id => ({ - id, - def: SPELLS_DEF[id], - source: store.equippedInstances ? - Object.entries(store.equippedInstances) - .filter(([, instId]) => instId && store.equipmentInstances[instId]?.enchantments.some(e => { - const effectDef = ENCHANTMENT_EFFECTS[e.effectId]; - return effectDef?.effect.type === 'spell' && effectDef.effect.spellId === id; - })) - .map(([slot]) => slot) - .join(', ') : '' - })).filter(s => s.def); - - return ( -
- {/* Equipment-Granted Spells */} -
-

✨ Known Spells

-

- Spells are obtained by enchanting equipment with spell effects. - Visit the Crafting tab to design and apply enchantments. -

- - {equipmentSpells.length > 0 ? ( -
- {equipmentSpells.map(({ id, def, source }) => { - const isActive = store.activeSpell === id; - const canCast = canAffordSpellCost(def.cost, store.rawMana, store.elements); - const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem]; - - return ( - - -
- - {def.name} - - Equipment -
-
- -
- {def.elem !== 'raw' && {elemDef?.sym} {elemDef?.name}} - ⚔️ {def.dmg} dmg -
-
- Cost: {formatSpellCost(def.cost)} -
-
From: {source}
-
- {isActive ? ( - Active - ) : ( - - )} -
-
-
- ); - })} -
- ) : ( -
-
No spells known yet
-
Enchant a staff with a spell effect to gain spells
-
- )} -
- - {/* Pact Spells (from guardian defeats) */} - {store.signedPacts.length > 0 && ( -
-

🏆 Pact Spells

-

Spells earned through guardian pacts appear here.

-
- )} - - {/* Spell Reference - show all available spells for enchanting */} -
-

📚 Spell Reference

-

- These spells can be applied to equipment through the enchanting system. - Research enchantment effects in the Skills tab to unlock them for designing. -

- -
- {Object.entries(SPELLS_DEF).map(([id, def]) => { - const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem]; - const isUnlocked = store.unlockedEffects?.includes(`spell_${id}`); - - return ( - - -
- - {def.name} - -
- {def.tier > 0 && T{def.tier}} - {isUnlocked && Unlocked} -
-
-
- -
- {def.elem !== 'raw' && {elemDef?.sym} {elemDef?.name}} - ⚔️ {def.dmg} dmg -
-
- Cost: {formatSpellCost(def.cost)} -
- {def.desc && ( -
{def.desc}
- )} - {!isUnlocked && ( -
Research to unlock for enchanting
- )} -
-
- ); - })} -
-
-
- ); - }; - - // Lab Tab - const renderLabTab = () => ( -
- {/* Elemental Mana Display */} - - - Elemental Mana - - -
- {Object.entries(store.elements) - .filter(([, state]) => state.unlocked) - .map(([id, state]) => { - const def = ELEMENTS[id]; - const isSelected = convertTarget === id; - return ( -
setConvertTarget(id)} - > -
{def?.sym}
-
{def?.name}
-
{state.current}/{state.max}
-
- ); - })} -
-
-
- - {/* Element Conversion */} - - - Element Conversion - - -

- Convert raw mana to elemental mana (100:1 ratio) -

- -
- - - -
-
-
- - {/* Unlock Elements */} - - - Unlock Elements - - -

- Unlock new elemental affinities (500 mana each) -

- -
- {Object.entries(store.elements) - .filter(([id, state]) => !state.unlocked && ELEMENTS[id]?.cat !== 'exotic') - .map(([id]) => { - const def = ELEMENTS[id]; - return ( -
-
{def?.sym}
-
{def?.name}
- -
- ); - })} -
-
-
- - {/* Composite Crafting */} - - - Composite & Exotic Crafting - - -
- {Object.entries(ELEMENTS) - .filter(([, def]) => def.recipe) - .map(([id, def]) => { - const state = store.elements[id]; - const recipe = def.recipe!; - const canCraft = recipe.every( - (r) => (store.elements[r]?.current || 0) >= recipe.filter((x) => x === r).length - ); - - return ( -
-
- {def.sym} -
-
- {def.name} -
-
{def.cat}
-
-
-
- {recipe.map((r) => ELEMENTS[r]?.sym).join(' + ')} -
- -
- ); - })} -
-
-
-
- ); // Check if skill has milestone available const hasMilestoneUpgrade = (skillId: string, level: number): { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null => { @@ -1279,7 +404,12 @@ export default function ManaLoopGame() { {store.currentStudyTarget && store.currentStudyTarget.type === 'skill' && ( - {renderStudyProgress()} + )} @@ -2236,7 +1366,7 @@ export default function ManaLoopGame() { {/* Calendar */}
- {renderCalendar()} +
@@ -2281,7 +1411,13 @@ export default function ManaLoopGame() {
Current Action
- {renderActionButtons()} +
@@ -2323,15 +1459,20 @@ export default function ManaLoopGame() { - {renderSpireTab()} + - {renderSpellsTab()} + - {renderLabTab()} + void; +} + +export function ActionButtons({ + currentAction, + designProgress, + preparationProgress, + applicationProgress, + setAction, +}: ActionButtonsProps) { + const actions: { id: GameAction; label: string; icon: typeof Swords }[] = [ + { id: 'meditate', label: 'Meditate', icon: Sparkles }, + { id: 'climb', label: 'Climb', icon: Swords }, + { id: 'study', label: 'Study', icon: BookOpen }, + { id: 'convert', label: 'Convert', icon: FlaskConical }, + ]; + + const hasDesignProgress = designProgress !== null; + const hasPrepProgress = preparationProgress !== null; + const hasAppProgress = applicationProgress !== null; + + return ( +
+
+ {actions.map(({ id, label, icon: Icon }) => ( + + ))} +
+ + {/* Crafting actions row - shown when there's active crafting progress */} + {(hasDesignProgress || hasPrepProgress || hasAppProgress) && ( +
+ + + +
+ )} +
+ ); +} diff --git a/src/components/game/CalendarDisplay.tsx b/src/components/game/CalendarDisplay.tsx new file mode 100644 index 0000000..c9e0995 --- /dev/null +++ b/src/components/game/CalendarDisplay.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants'; + +interface CalendarDisplayProps { + currentDay: number; +} + +export function CalendarDisplay({ currentDay }: CalendarDisplayProps) { + const days: React.ReactElement[] = []; + + for (let d = 1; d <= MAX_DAY; d++) { + let dayClass = 'w-7 h-7 rounded text-xs flex items-center justify-center font-mono border transition-all '; + + if (d < currentDay) { + dayClass += 'bg-blue-900/30 border-blue-800/50 text-blue-400'; + } else if (d === currentDay) { + dayClass += 'bg-blue-600/40 border-blue-500 text-blue-300 shadow-lg shadow-blue-500/30'; + } else { + dayClass += 'bg-gray-800/30 border-gray-700/50 text-gray-500'; + } + + if (d >= INCURSION_START_DAY) { + dayClass += ' border-red-600/50'; + } + + days.push( + + +
+ {d} +
+
+ +

Day {d}

+ {d >= INCURSION_START_DAY &&

Incursion Active

} +
+
+ ); + } + + return <>{days}; +} diff --git a/src/components/game/CraftingProgress.tsx b/src/components/game/CraftingProgress.tsx new file mode 100644 index 0000000..66887ff --- /dev/null +++ b/src/components/game/CraftingProgress.tsx @@ -0,0 +1,161 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { Target, FlaskConical, Sparkles, Play, Pause, X } from 'lucide-react'; +import { fmt } from '@/lib/game/store'; +import { formatStudyTime } from '@/lib/game/formatting'; +import type { EquipmentInstance, EnchantmentDesign } from '@/lib/game/types'; + +interface CraftingProgressProps { + designProgress: { designId: string; progress: number; required: number } | null; + preparationProgress: { equipmentInstanceId: string; progress: number; required: number; manaCostPaid: number } | null; + applicationProgress: { equipmentInstanceId: string; designId: string; progress: number; required: number; manaPerHour: number; paused: boolean } | null; + equipmentInstances: Record; + enchantmentDesigns: EnchantmentDesign[]; + cancelDesign: () => void; + cancelPreparation: () => void; + pauseApplication: () => void; + resumeApplication: () => void; + cancelApplication: () => void; +} + +export function CraftingProgress({ + designProgress, + preparationProgress, + applicationProgress, + equipmentInstances, + enchantmentDesigns, + cancelDesign, + cancelPreparation, + pauseApplication, + resumeApplication, + cancelApplication, +}: CraftingProgressProps) { + const progressSections: React.ReactNode[] = []; + + // Design progress + if (designProgress) { + const progressPct = Math.min(100, (designProgress.progress / designProgress.required) * 100); + progressSections.push( +
+
+
+ + + Designing Enchantment + +
+ +
+ +
+ {formatStudyTime(designProgress.progress)} / {formatStudyTime(designProgress.required)} + Design Time +
+
+ ); + } + + // Preparation progress + if (preparationProgress) { + const progressPct = Math.min(100, (preparationProgress.progress / preparationProgress.required) * 100); + const instance = equipmentInstances[preparationProgress.equipmentInstanceId]; + progressSections.push( +
+
+
+ + + Preparing {instance?.name || 'Equipment'} + +
+ +
+ +
+ {formatStudyTime(preparationProgress.progress)} / {formatStudyTime(preparationProgress.required)} + Mana spent: {fmt(preparationProgress.manaCostPaid)} +
+
+ ); + } + + // Application progress + if (applicationProgress) { + const progressPct = Math.min(100, (applicationProgress.progress / applicationProgress.required) * 100); + const instance = equipmentInstances[applicationProgress.equipmentInstanceId]; + const design = enchantmentDesigns.find(d => d.id === applicationProgress.designId); + progressSections.push( +
+
+
+ + + Enchanting {instance?.name || 'Equipment'} + +
+
+ {applicationProgress.paused ? ( + + ) : ( + + )} + +
+
+ +
+ {formatStudyTime(applicationProgress.progress)} / {formatStudyTime(applicationProgress.required)} + Mana/hr: {fmt(applicationProgress.manaPerHour)} +
+ {design && ( +
+ Applying: {design.name} +
+ )} +
+ ); + } + + return progressSections.length > 0 ? ( +
+ {progressSections} +
+ ) : null; +} diff --git a/src/components/game/ManaDisplay.tsx b/src/components/game/ManaDisplay.tsx new file mode 100644 index 0000000..280049d --- /dev/null +++ b/src/components/game/ManaDisplay.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { Card, CardContent } from '@/components/ui/card'; +import { Zap } from 'lucide-react'; +import { fmt, fmtDec } from '@/lib/game/store'; + +interface ManaDisplayProps { + rawMana: number; + maxMana: number; + effectiveRegen: number; + meditationMultiplier: number; + clickMana: number; + isGathering: boolean; + onGatherStart: () => void; + onGatherEnd: () => void; +} + +export function ManaDisplay({ + rawMana, + maxMana, + effectiveRegen, + meditationMultiplier, + clickMana, + isGathering, + onGatherStart, + onGatherEnd, +}: ManaDisplayProps) { + return ( + + +
+
+ {fmt(rawMana)} + / {fmt(maxMana)} +
+
+ +{fmtDec(effectiveRegen)} mana/hr {meditationMultiplier > 1.01 && ({fmtDec(meditationMultiplier, 1)}x med)} +
+
+ + + + +
+
+ ); +} diff --git a/src/components/game/StudyProgress.tsx b/src/components/game/StudyProgress.tsx new file mode 100644 index 0000000..c4127b6 --- /dev/null +++ b/src/components/game/StudyProgress.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { BookOpen, X } from 'lucide-react'; +import { SKILLS_DEF, SPELLS_DEF } from '@/lib/game/constants'; +import { formatStudyTime } from '@/lib/game/formatting'; +import type { StudyTarget } from '@/lib/game/types'; + +interface StudyProgressProps { + currentStudyTarget: StudyTarget | null; + skills: Record; + studySpeedMult: number; + cancelStudy: () => void; +} + +export function StudyProgress({ + currentStudyTarget, + skills, + studySpeedMult, + cancelStudy, +}: StudyProgressProps) { + if (!currentStudyTarget) return null; + + const target = currentStudyTarget; + 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 ? (skills[target.id] || 0) : 0; + + return ( +
+
+
+ + + {def?.name} + {isSkill && ` Lv.${currentLevel + 1}`} + +
+ +
+ +
+ {formatStudyTime(target.progress)} / {formatStudyTime(target.required)} + {studySpeedMult.toFixed(1)}x speed +
+
+ ); +} diff --git a/src/components/game/TimeDisplay.tsx b/src/components/game/TimeDisplay.tsx new file mode 100644 index 0000000..5916e52 --- /dev/null +++ b/src/components/game/TimeDisplay.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { Play, Pause } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { fmt } from '@/lib/game/store'; +import { formatHour } from '@/lib/game/formatting'; + +interface TimeDisplayProps { + day: number; + hour: number; + insight: number; + paused: boolean; + onTogglePause: () => void; +} + +export function TimeDisplay({ + day, + hour, + insight, + paused, + onTogglePause, +}: TimeDisplayProps) { + return ( +
+
+
+ Day {day} +
+
+ {formatHour(hour)} +
+
+ +
+
+ {fmt(insight)} +
+
Insight
+
+ + +
+ ); +} diff --git a/src/components/game/index.ts b/src/components/game/index.ts index 3c0b16d..a280615 100755 --- a/src/components/game/index.ts +++ b/src/components/game/index.ts @@ -1,7 +1,17 @@ // ─── Game Components Index ────────────────────────────────────────────────────── // Re-exports all game tab components for cleaner imports +// Tab components export { CraftingTab } from './tabs/CraftingTab'; export { SpireTab } from './tabs/SpireTab'; export { SpellsTab } from './tabs/SpellsTab'; export { LabTab } from './tabs/LabTab'; + +// UI components +export { ActionButtons } from './ActionButtons'; +export { CalendarDisplay } from './CalendarDisplay'; +export { ComboMeter } from './ComboMeter'; +export { CraftingProgress } from './CraftingProgress'; +export { StudyProgress } from './StudyProgress'; +export { ManaDisplay } from './ManaDisplay'; +export { TimeDisplay } from './TimeDisplay'; diff --git a/src/components/game/tabs/SpireTab.tsx b/src/components/game/tabs/SpireTab.tsx index f6fada5..9e34fdd 100755 --- a/src/components/game/tabs/SpireTab.tsx +++ b/src/components/game/tabs/SpireTab.tsx @@ -6,13 +6,47 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { Swords, Sparkles, BookOpen, ChevronUp, ChevronDown, ArrowUp, ArrowDown, RefreshCw } from 'lucide-react'; -import type { GameState, GameAction } from '@/lib/game/types'; -import { ELEMENTS, GUARDIANS, SPELLS_DEF, MAX_DAY, INCURSION_START_DAY } from '@/lib/game/constants'; +import { TooltipProvider } from '@/components/ui/tooltip'; +import { Swords, BookOpen, ChevronUp, ChevronDown, RotateCcw, X } from 'lucide-react'; +import type { GameState, GameAction, EquipmentSpellState, StudyTarget } from '@/lib/game/types'; +import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants'; import { fmt, fmtDec, getFloorElement, calcDamage, canAffordSpellCost } from '@/lib/game/store'; import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting'; -import type { ComputedEffects } from '@/lib/game/effects'; +import { ComboMeter, CraftingProgress, StudyProgress } from '@/components/game'; +import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; +import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; + +// Helper to get active spells from equipped caster weapons +function getActiveEquipmentSpells( + equippedInstances: Record, + equipmentInstances: Record }> +): Array<{ spellId: string; equipmentId: string }> { + const spells: Array<{ spellId: string; equipmentId: string }> = []; + const weaponSlots = ['mainHand', 'offHand'] as const; + + for (const slot of weaponSlots) { + const instanceId = equippedInstances[slot]; + if (!instanceId) continue; + + const instance = equipmentInstances[instanceId]; + if (!instance) continue; + + const equipType = EQUIPMENT_TYPES[instance.typeId]; + if (!equipType || equipType.category !== 'caster') continue; + + for (const ench of instance.enchantments) { + const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; + if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { + spells.push({ + spellId: effectDef.effect.spellId, + equipmentId: instanceId, + }); + } + } + } + + return spells; +} interface SpireTabProps { store: GameState & { @@ -22,80 +56,42 @@ interface SpireTabProps { cancelParallelStudy: () => void; setClimbDirection: (direction: 'up' | 'down') => void; changeFloor: (direction: 'up' | 'down') => void; + cancelDesign?: () => void; + cancelPreparation?: () => void; + pauseApplication?: () => void; + resumeApplication?: () => void; + cancelApplication?: () => void; }; - upgradeEffects: ComputedEffects; - maxMana: number; - effectiveRegen: number; - incursionStrength: number; - dps: number; + totalDPS: number; studySpeedMult: number; - formatTime: (hour: number) => string; + incursionStrength: number; } export function SpireTab({ store, - upgradeEffects, - maxMana, - effectiveRegen, - incursionStrength, - dps, + totalDPS, studySpeedMult, - formatTime, + incursionStrength, }: SpireTabProps) { const floorElem = getFloorElement(store.currentFloor); const floorElemDef = ELEMENTS[floorElem]; const isGuardianFloor = !!GUARDIANS[store.currentFloor]; const currentGuardian = GUARDIANS[store.currentFloor]; - const activeSpellDef = SPELLS_DEF[store.activeSpell]; const climbDirection = store.climbDirection || 'up'; const clearedFloors = store.clearedFloors || {}; // Check if current floor is cleared (for respawn indicator) const isFloorCleared = clearedFloors[store.currentFloor]; + // Get active equipment spells + const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances); + const canCastSpell = (spellId: string): boolean => { const spell = SPELLS_DEF[spellId]; if (!spell) return false; return canAffordSpellCost(spell.cost, store.rawMana, store.elements); }; - // Render study progress - const renderStudyProgress = () => { - if (!store.currentStudyTarget) return null; - - const target = store.currentStudyTarget; - const progressPct = Math.min(100, (target.progress / target.required) * 100); - const isSkill = target.type === 'skill'; - const def = isSkill ? SPELLS_DEF[target.id] : SPELLS_DEF[target.id]; - - return ( -
-
-
- - - {def?.name} - {isSkill && ` Lv.${(store.skills[target.id] || 0) + 1}`} - -
- -
- -
- {formatStudyTime(target.progress)} / {formatStudyTime(target.required)} - {studySpeedMult.toFixed(1)}x speed -
-
- ); - }; - return (
@@ -138,16 +134,16 @@ export function SpireTab({
{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP - DPS: {store.currentAction === 'climb' && canCastSpell(store.activeSpell) ? fmtDec(dps) : '—'} + DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}
- {/* Floor Navigation */} + {/* Floor Navigation Controls */}
- Direction + Auto-Direction
@@ -179,7 +175,7 @@ export function SpireTab({ onClick={() => store.changeFloor('down')} > - Go Down + Descend
{isFloorCleared && ( -
- ⚠️ Floor will respawn when you return +
+ + Floor will respawn when you leave and return
)}
@@ -209,51 +206,74 @@ export function SpireTab({ - {/* Active Spell Card */} + {/* Active Spells Card - Shows all spells from equipped weapons */} - Active Spell + + Active Spells ({activeEquipmentSpells.length}) + - {activeSpellDef ? ( - <> -
- {activeSpellDef.name} - {activeSpellDef.tier === 0 && Basic} - {activeSpellDef.tier >= 4 && Legendary} -
-
- ⚔️ {fmt(calcDamage(store, store.activeSpell))} dmg • - - {' '}{formatSpellCost(activeSpellDef.cost)} - - {' '}• ⚡ {(activeSpellDef.castSpeed || 1).toFixed(1)} casts/hr -
- - {/* Cast progress bar when climbing */} - {store.currentAction === 'climb' && ( -
-
- Cast Progress - {((store.castProgress || 0) * 100).toFixed(0)}% + {activeEquipmentSpells.length > 0 ? ( +
+ {activeEquipmentSpells.map(({ spellId, equipmentId }) => { + const spellDef = SPELLS_DEF[spellId]; + if (!spellDef) return null; + + const spellState = store.equipmentSpellStates?.find( + s => s.spellId === spellId && s.sourceEquipment === equipmentId + ); + const progress = spellState?.castProgress || 0; + const canCast = canAffordSpellCost(spellDef.cost, store.rawMana, store.elements); + + return ( +
+
+
+ {spellDef.name} + {spellDef.tier === 0 && Basic} + {spellDef.tier >= 4 && Legendary} +
+ + {canCast ? '✓' : '✗'} + +
+
+ ⚔️ {fmt(calcDamage(store, spellId, floorElem))} dmg • + + {' '}{formatSpellCost(spellDef.cost)} + + {' '}• ⚡ {(spellDef.castSpeed || 1).toFixed(1)}/hr +
+ + {/* Cast progress bar when climbing */} + {store.currentAction === 'climb' && ( +
+
+ Cast + {(progress * 100).toFixed(0)}% +
+ +
+ )} + + {spellDef.effects && spellDef.effects.length > 0 && ( +
+ {spellDef.effects.map((eff, i) => ( + + {eff.type === 'lifesteal' && `🩸 ${Math.round(eff.value * 100)}%`} + {eff.type === 'burn' && `🔥 Burn`} + {eff.type === 'freeze' && `❄️ Freeze`} + + ))} +
+ )}
- -
- )} - - {activeSpellDef.desc && ( -
{activeSpellDef.desc}
- )} - - ) : ( -
No spell selected
- )} - - {/* Can cast indicator */} - {activeSpellDef && ( -
- {canCastSpell(store.activeSpell) ? '✓ Can cast' : '✗ Insufficient mana'} + ); + })}
+ ) : ( +
No spells on equipped weapons. Enchant a staff with spell effects.
)} {incursionStrength > 0 && ( @@ -267,11 +287,67 @@ export function SpireTab({ + {/* Combo Meter - Shows when climbing or has active combo */} + + {/* Current Study (if any) */} {store.currentStudyTarget && ( - {renderStudyProgress()} + + + {/* Parallel Study Progress */} + {store.parallelStudyTarget && ( +
+
+
+ + + Parallel: {SKILLS_DEF[store.parallelStudyTarget.id]?.name} + {store.parallelStudyTarget.type === 'skill' && ` Lv.${(store.skills[store.parallelStudyTarget.id] || 0) + 1}`} + +
+ +
+ +
+ {formatStudyTime(store.parallelStudyTarget.progress)} / {formatStudyTime(store.parallelStudyTarget.required)} + 50% speed (Parallel Study) +
+
+ )} +
+
+ )} + + {/* Crafting Progress (if any) */} + {(store.designProgress || store.preparationProgress || store.applicationProgress) && ( + + + )} diff --git a/src/components/game/tabs/index.ts b/src/components/game/tabs/index.ts new file mode 100644 index 0000000..85594e9 --- /dev/null +++ b/src/components/game/tabs/index.ts @@ -0,0 +1,7 @@ +// ─── Tab Components Index ────────────────────────────────────────────────────── +// Re-exports all tab components for cleaner imports + +export { CraftingTab } from './CraftingTab'; +export { SpireTab } from './SpireTab'; +export { SpellsTab } from './SpellsTab'; +export { LabTab } from './LabTab'; diff --git a/src/lib/game/computed-stats.ts b/src/lib/game/computed-stats.ts new file mode 100644 index 0000000..edad755 --- /dev/null +++ b/src/lib/game/computed-stats.ts @@ -0,0 +1,397 @@ +// ─── Computed Stats and Utility Functions ─────────────────────────────────────── +// This module contains all computed stat functions and utility helpers +// extracted from the main store for better organization + +import type { GameState, SpellCost, EquipmentInstance } from './types'; +import { + GUARDIANS, + SPELLS_DEF, + FLOOR_ELEM_CYCLE, + HOURS_PER_TICK, + MAX_DAY, + INCURSION_START_DAY, + ELEMENT_OPPOSITES, +} from './constants'; +import type { ComputedEffects } from './upgrade-effects'; +import { getUnifiedEffects, type UnifiedEffects } from './effects'; +import { EQUIPMENT_TYPES } from './data/equipment'; +import { ENCHANTMENT_EFFECTS } from './data/enchantment-effects'; + +// ─── Default Effects Constant ─────────────────────────────────────────────────── + +// Default empty effects for when effects aren't provided +export const DEFAULT_EFFECTS: ComputedEffects = { + maxManaMultiplier: 1, + maxManaBonus: 0, + regenMultiplier: 1, + regenBonus: 0, + clickManaMultiplier: 1, + clickManaBonus: 0, + meditationEfficiency: 1, + spellCostMultiplier: 1, + conversionEfficiency: 1, + baseDamageMultiplier: 1, + baseDamageBonus: 0, + attackSpeedMultiplier: 1, + critChanceBonus: 0, + critDamageMultiplier: 1.5, + elementalDamageMultiplier: 1, + studySpeedMultiplier: 1, + studyCostMultiplier: 1, + progressRetention: 0, + instantStudyChance: 0, + freeStudyChance: 0, + elementCapMultiplier: 1, + elementCapBonus: 0, + conversionCostMultiplier: 1, + doubleCraftChance: 0, + permanentRegenBonus: 0, + specials: new Set(), + activeUpgrades: [], +}; + +// ─── Number Formatting Functions ──────────────────────────────────────────────── + +export function fmt(n: number): string { + if (!isFinite(n) || isNaN(n)) return '0'; + if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B'; + if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'; + if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'; + return Math.floor(n).toString(); +} + +export function fmtDec(n: number, d: number = 1): string { + return isFinite(n) ? n.toFixed(d) : '0'; +} + +// ─── Floor Functions ──────────────────────────────────────────────────────────── + +export function getFloorMaxHP(floor: number): number { + if (GUARDIANS[floor]) return GUARDIANS[floor].hp; + // Improved scaling: slower early game, faster late game + const baseHP = 100; + const floorScaling = floor * 50; + const exponentialScaling = Math.pow(floor, 1.7); + return Math.floor(baseHP + floorScaling + exponentialScaling); +} + +export function getFloorElement(floor: number): string { + return FLOOR_ELEM_CYCLE[(floor - 1) % 8]; +} + +// ─── Equipment Spell Helper ───────────────────────────────────────────────────── + +// Get all spells from equipped caster weapons (staves, wands, etc.) +// Returns array of { spellId, equipmentInstanceId } +export function getActiveEquipmentSpells( + equippedInstances: Record, + equipmentInstances: Record +): Array<{ spellId: string; equipmentId: string }> { + const spells: Array<{ spellId: string; equipmentId: string }> = []; + + // Check main hand and off hand for caster equipment + const weaponSlots = ['mainHand', 'offHand'] as const; + + for (const slot of weaponSlots) { + const instanceId = equippedInstances[slot]; + if (!instanceId) continue; + + const instance = equipmentInstances[instanceId]; + if (!instance) continue; + + // Check if this is a caster-type equipment + const equipType = EQUIPMENT_TYPES[instance.typeId]; + if (!equipType || equipType.category !== 'caster') continue; + + // Get spells from enchantments + for (const ench of instance.enchantments) { + const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; + if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { + spells.push({ + spellId: effectDef.effect.spellId, + equipmentId: instanceId, + }); + } + } + } + + return spells; +} + +// ─── Skill Level Helper ───────────────────────────────────────────────────────── + +// Helper to get effective skill level accounting for tiers +export function getEffectiveSkillLevel( + skills: Record, + baseSkillId: string, + skillTiers: Record = {} +): { level: number; tier: number; tierMultiplier: number } { + // Find the highest tier the player has for this base skill + const currentTier = skillTiers[baseSkillId] || 1; + + // Look for the tiered skill ID (e.g., manaFlow_t2) + const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId; + const level = skills[tieredSkillId] || skills[baseSkillId] || 0; + + // Tier multiplier: each tier is 10x more powerful + const tierMultiplier = Math.pow(10, currentTier - 1); + + return { level, tier: currentTier, tierMultiplier }; +} + +// ─── Computed Stat Functions ──────────────────────────────────────────────────── + +export function computeMaxMana( + state: Pick, + effects?: ComputedEffects | UnifiedEffects +): number { + const pu = state.prestigeUpgrades; + const base = + 100 + + (state.skills.manaWell || 0) * 100 + + (pu.manaWell || 0) * 500; + + // If effects not provided, compute unified effects (includes equipment) + if (!effects && state.equipmentInstances && state.equippedInstances) { + effects = getUnifiedEffects(state as any); + } + + // Apply effects if available (now includes equipment bonuses) + if (effects) { + return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier); + } + return base; +} + +export function computeElementMax( + state: Pick, + effects?: ComputedEffects +): number { + const pu = state.prestigeUpgrades; + const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25; + + // Apply upgrade effects if provided + if (effects) { + return Math.floor((base + effects.elementCapBonus) * effects.elementCapMultiplier); + } + return base; +} + +export function computeRegen( + state: Pick, + effects?: ComputedEffects | UnifiedEffects +): number { + const pu = state.prestigeUpgrades; + const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1; + const base = + 2 + + (state.skills.manaFlow || 0) * 1 + + (state.skills.manaSpring || 0) * 2 + + (pu.manaFlow || 0) * 0.5; + + let regen = base * temporalBonus; + + // If effects not provided, compute unified effects (includes equipment) + if (!effects && state.equipmentInstances && state.equippedInstances) { + effects = getUnifiedEffects(state as any); + } + + // Apply effects if available (now includes equipment bonuses) + if (effects) { + regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier; + } + + return regen; +} + +/** + * Compute regen with dynamic special effects (needs current mana, max mana, incursion) + */ +export function computeEffectiveRegen( + state: Pick, + effects?: ComputedEffects | UnifiedEffects +): number { + // Base regen from existing function + let regen = computeRegen(state, effects); + + const maxMana = computeMaxMana(state, effects); + const currentMana = state.rawMana; + const incursionStrength = state.incursionStrength || 0; + + // Apply incursion penalty + regen *= (1 - incursionStrength); + + return regen; +} + +export function computeClickMana( + state: Pick, + effects?: ComputedEffects | UnifiedEffects +): number { + const base = + 1 + + (state.skills.manaTap || 0) * 1 + + (state.skills.manaSurge || 0) * 3; + + // If effects not provided, compute unified effects (includes equipment) + if (!effects && state.equipmentInstances && state.equippedInstances) { + effects = getUnifiedEffects(state as any); + } + + // Apply effects if available (now includes equipment bonuses) + if (effects) { + return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier); + } + return base; +} + +// ─── Damage Calculation Helpers ───────────────────────────────────────────────── + +// Elemental damage bonus: +50% if spell element opposes floor element (super effective) +// -25% if spell element matches its own opposite (weak) +export function getElementalBonus(spellElem: string, floorElem: string): number { + if (spellElem === 'raw') return 1.0; // Raw mana has no elemental bonus + + if (spellElem === floorElem) return 1.25; // Same element: +25% damage + + // Check for super effective first: spell is the opposite of floor + // e.g., casting water (opposite of fire) at fire floor = super effective + if (ELEMENT_OPPOSITES[floorElem] === spellElem) return 1.5; // Super effective: +50% damage + + // Check for weak: spell's opposite matches floor + // e.g., casting fire (whose opposite is water) at water floor = weak + if (ELEMENT_OPPOSITES[spellElem] === floorElem) return 0.75; // Weak: -25% damage + + return 1.0; // Neutral +} + +export function calcDamage( + state: Pick, + spellId: string, + floorElem?: string +): number { + const sp = SPELLS_DEF[spellId]; + if (!sp) return 5; + const skills = state.skills; + const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5; + const pct = 1 + (skills.arcaneFury || 0) * 0.1; + + // Elemental mastery bonus + const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15; + + // Guardian bane bonus + const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0] + ? 1 + (skills.guardianBane || 0) * 0.2 + : 1; + + const critChance = (skills.precision || 0) * 0.05; + const pactMult = state.signedPacts.reduce( + (m, f) => m * (GUARDIANS[f]?.pact || 1), + 1 + ); + + let damage = baseDmg * pct * pactMult * elemMasteryBonus; + + // Apply elemental bonus if floor element provided + if (floorElem) { + damage *= getElementalBonus(sp.elem, floorElem); + } + + // Apply crit + if (Math.random() < critChance) { + damage *= 1.5; + } + + return damage; +} + +// ─── Insight Calculation ──────────────────────────────────────────────────────── + +export function calcInsight(state: Pick): number { + const pu = state.prestigeUpgrades; + const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1; + const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus; + return Math.floor( + (state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult + ); +} + +// ─── Meditation Bonus ─────────────────────────────────────────────────────────── + +// Meditation bonus now affects regen rate directly +export function getMeditationBonus(meditateTicks: number, skills: Record, 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; + + // Base meditation: ramps up over 4 hours to 1.5x + let bonus = 1 + Math.min(hours / 4, 0.5); + + // With Meditation Focus: up to 2.5x after 4 hours + if (hasMeditation && hours >= 4) { + bonus = 2.5; + } + + // With Deep Trance: up to 3.0x after 6 hours + if (hasDeepTrance && hours >= 6) { + bonus = 3.0; + } + + // With Void Meditation: up to 5.0x after 8 hours + if (hasVoidMeditation && hours >= 8) { + bonus = 5.0; + } + + // Apply meditation efficiency from upgrades (Deep Wellspring, etc.) + bonus *= meditationEfficiency; + + return bonus; +} + +// ─── Incursion Strength ───────────────────────────────────────────────────────── + +export function getIncursionStrength(day: number, hour: number): number { + if (day < INCURSION_START_DAY) return 0; + const totalHours = (day - INCURSION_START_DAY) * 24 + hour; + const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24; + return Math.min(0.95, (totalHours / maxHours) * 0.95); +} + +// ─── Spell Cost Helpers ───────────────────────────────────────────────────────── + +// Check if player can afford spell cost +export function canAffordSpellCost( + cost: SpellCost, + rawMana: number, + elements: Record +): boolean { + if (cost.type === 'raw') { + return rawMana >= cost.amount; + } else { + const elem = elements[cost.element || '']; + return elem && elem.unlocked && elem.current >= cost.amount; + } +} + +// Deduct spell cost from appropriate mana pool +export function deductSpellCost( + cost: SpellCost, + rawMana: number, + elements: Record +): { rawMana: number; elements: Record } { + const newElements = { ...elements }; + + if (cost.type === 'raw') { + return { rawMana: rawMana - cost.amount, elements: newElements }; + } else if (cost.element && newElements[cost.element]) { + newElements[cost.element] = { + ...newElements[cost.element], + current: newElements[cost.element].current - cost.amount + }; + return { rawMana, elements: newElements }; + } + + return { rawMana, elements: newElements }; +} diff --git a/src/lib/game/formatting.ts b/src/lib/game/formatting.ts index 6b91d9c..0da5648 100755 --- a/src/lib/game/formatting.ts +++ b/src/lib/game/formatting.ts @@ -4,6 +4,9 @@ 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 */ diff --git a/src/lib/game/navigation-slice.ts b/src/lib/game/navigation-slice.ts new file mode 100644 index 0000000..dc254f2 --- /dev/null +++ b/src/lib/game/navigation-slice.ts @@ -0,0 +1,63 @@ +// ─── 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; +} + +// ─── Navigation Slice Factory ───────────────────────────────────────────────── + +export function createNavigationSlice( + set: (partial: Partial | ((state: GameState) => Partial)) => 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)], + }); + }, + }; +} diff --git a/src/lib/game/store.ts b/src/lib/game/store.ts index 11bc0ee..9a40c46 100755 --- a/src/lib/game/store.ts +++ b/src/lib/game/store.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; -import type { GameState, GameAction, StudyTarget, SpellCost, SkillUpgradeChoice, EquipmentSlot, EquipmentInstance, EnchantmentDesign, DesignEffect, LootInventory } from './types'; +import type { GameState, GameAction, StudyTarget, SkillUpgradeChoice, EquipmentSlot, EnchantmentDesign, DesignEffect, LootInventory } from './types'; import { ELEMENTS, GUARDIANS, @@ -17,19 +17,13 @@ import { INCURSION_START_DAY, MANA_PER_ELEMENT, getStudySpeedMultiplier, - getStudyCostMultiplier, ELEMENT_OPPOSITES, EFFECT_RESEARCH_MAPPING, BASE_UNLOCKED_EFFECTS, ENCHANTING_UNLOCK_EFFECTS, } from './constants'; -import { computeEffects, hasSpecial, SPECIAL_EFFECTS, type ComputedEffects } from './upgrade-effects'; -import { - computeAllEffects, - getUnifiedEffects, - computeEquipmentEffects, - type UnifiedEffects -} from './effects'; +import { hasSpecial, SPECIAL_EFFECTS } from './upgrade-effects'; +import { getUnifiedEffects } from './effects'; import { SKILL_EVOLUTION_PATHS } from './skill-evolution'; import { createStartingEquipment, @@ -37,7 +31,6 @@ import { getSpellsFromEquipment, type CraftingActions } from './crafting-slice'; -import { EQUIPMENT_TYPES } from './data/equipment'; import { ENCHANTMENT_EFFECTS, calculateEffectCapacityCost } from './data/enchantment-effects'; import { createFamiliarSlice, @@ -47,228 +40,56 @@ import { type FamiliarBonuses, DEFAULT_FAMILIAR_BONUSES, } from './familiar-slice'; +import { + createNavigationSlice, + type NavigationActions, +} from './navigation-slice'; +import { + createStudySlice, + type StudyActions, +} from './study-slice'; import { rollLootDrops, LOOT_DROPS } from './data/loot-drops'; import { CRAFTING_RECIPES, canCraftRecipe } from './data/crafting-recipes'; +import { EQUIPMENT_TYPES } from './data/equipment'; +import type { EquipmentInstance } from './types'; +// Import computed stats and utility functions from computed-stats.ts +import { + DEFAULT_EFFECTS, + fmt, + fmtDec, + getFloorMaxHP, + getFloorElement, + getActiveEquipmentSpells, + getEffectiveSkillLevel, + computeMaxMana, + computeElementMax, + computeRegen, + computeEffectiveRegen, + computeClickMana, + calcDamage, + calcInsight, + getMeditationBonus, + getIncursionStrength, + canAffordSpellCost, + deductSpellCost, +} from './computed-stats'; -// Default empty effects for when effects aren't provided -const DEFAULT_EFFECTS: ComputedEffects = { - maxManaMultiplier: 1, - maxManaBonus: 0, - regenMultiplier: 1, - regenBonus: 0, - clickManaMultiplier: 1, - clickManaBonus: 0, - meditationEfficiency: 1, - spellCostMultiplier: 1, - conversionEfficiency: 1, - baseDamageMultiplier: 1, - baseDamageBonus: 0, - attackSpeedMultiplier: 1, - critChanceBonus: 0, - critDamageMultiplier: 1.5, - elementalDamageMultiplier: 1, - studySpeedMultiplier: 1, - studyCostMultiplier: 1, - progressRetention: 0, - instantStudyChance: 0, - freeStudyChance: 0, - elementCapMultiplier: 1, - elementCapBonus: 0, - conversionCostMultiplier: 1, - doubleCraftChance: 0, - permanentRegenBonus: 0, - specials: new Set(), - activeUpgrades: [], +// Re-export formatting functions and computed stats for backward compatibility +export { + fmt, + fmtDec, + getFloorElement, + computeMaxMana, + computeRegen, + computeClickMana, + calcDamage, + getMeditationBonus, + getIncursionStrength, + canAffordSpellCost, + getFloorMaxHP, }; -// ─── Helper Functions ───────────────────────────────────────────────────────── - -export function fmt(n: number): string { - if (!isFinite(n) || isNaN(n)) return '0'; - if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B'; - if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'; - if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'; - return Math.floor(n).toString(); -} - -export function fmtDec(n: number, d: number = 1): string { - return isFinite(n) ? n.toFixed(d) : '0'; -} - -export function getFloorMaxHP(floor: number): number { - if (GUARDIANS[floor]) return GUARDIANS[floor].hp; - // Improved scaling: slower early game, faster late game - const baseHP = 100; - const floorScaling = floor * 50; - const exponentialScaling = Math.pow(floor, 1.7); - return Math.floor(baseHP + floorScaling + exponentialScaling); -} - -export function getFloorElement(floor: number): string { - return FLOOR_ELEM_CYCLE[(floor - 1) % 8]; -} - -// Get all spells from equipped caster weapons (staves, wands, etc.) -// Returns array of { spellId, equipmentInstanceId } -function getActiveEquipmentSpells( - equippedInstances: Record, - equipmentInstances: Record -): Array<{ spellId: string; equipmentId: string }> { - const spells: Array<{ spellId: string; equipmentId: string }> = []; - - // Check main hand and off hand for caster equipment - const weaponSlots = ['mainHand', 'offHand'] as const; - - for (const slot of weaponSlots) { - const instanceId = equippedInstances[slot]; - if (!instanceId) continue; - - const instance = equipmentInstances[instanceId]; - if (!instance) continue; - - // Check if this is a caster-type equipment - const equipType = EQUIPMENT_TYPES[instance.typeId]; - if (!equipType || equipType.category !== 'caster') continue; - - // Get spells from enchantments - for (const ench of instance.enchantments) { - const effectDef = ENCHANTMENT_EFFECTS[ench.effectId]; - if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) { - spells.push({ - spellId: effectDef.effect.spellId, - equipmentId: instanceId, - }); - } - } - } - - return spells; -} - -// ─── Computed Stats Functions ───────────────────────────────────────────────── - -// Helper to get effective skill level accounting for tiers -function getEffectiveSkillLevel( - skills: Record, - baseSkillId: string, - skillTiers: Record = {} -): { level: number; tier: number; tierMultiplier: number } { - // Find the highest tier the player has for this base skill - const currentTier = skillTiers[baseSkillId] || 1; - - // Look for the tiered skill ID (e.g., manaFlow_t2) - const tieredSkillId = currentTier > 1 ? `${baseSkillId}_t${currentTier}` : baseSkillId; - const level = skills[tieredSkillId] || skills[baseSkillId] || 0; - - // Tier multiplier: each tier is 10x more powerful - const tierMultiplier = Math.pow(10, currentTier - 1); - - return { level, tier: currentTier, tierMultiplier }; -} - -export function computeMaxMana( - state: Pick, - effects?: ComputedEffects | UnifiedEffects -): number { - const pu = state.prestigeUpgrades; - const base = - 100 + - (state.skills.manaWell || 0) * 100 + - (pu.manaWell || 0) * 500; - - // If effects not provided, compute unified effects (includes equipment) - if (!effects && state.equipmentInstances && state.equippedInstances) { - effects = getUnifiedEffects(state as any); - } - - // Apply effects if available (now includes equipment bonuses) - if (effects) { - return Math.floor((base + effects.maxManaBonus) * effects.maxManaMultiplier); - } - return base; -} - -export function computeElementMax( - state: Pick, - effects?: ComputedEffects -): number { - const pu = state.prestigeUpgrades; - const base = 10 + (state.skills.elemAttune || 0) * 50 + (pu.elementalAttune || 0) * 25; - - // Apply upgrade effects if provided - if (effects) { - return Math.floor((base + effects.elementCapBonus) * effects.elementCapMultiplier); - } - return base; -} - -export function computeRegen( - state: Pick, - effects?: ComputedEffects | UnifiedEffects -): number { - const pu = state.prestigeUpgrades; - const temporalBonus = 1 + (pu.temporalEcho || 0) * 0.1; - const base = - 2 + - (state.skills.manaFlow || 0) * 1 + - (state.skills.manaSpring || 0) * 2 + - (pu.manaFlow || 0) * 0.5; - - let regen = base * temporalBonus; - - // If effects not provided, compute unified effects (includes equipment) - if (!effects && state.equipmentInstances && state.equippedInstances) { - effects = getUnifiedEffects(state as any); - } - - // Apply effects if available (now includes equipment bonuses) - if (effects) { - regen = (regen + effects.regenBonus + effects.permanentRegenBonus) * effects.regenMultiplier; - } - - return regen; -} - -/** - * Compute regen with dynamic special effects (needs current mana, max mana, incursion) - */ -export function computeEffectiveRegen( - state: Pick, - effects?: ComputedEffects | UnifiedEffects -): number { - // Base regen from existing function - let regen = computeRegen(state, effects); - - const maxMana = computeMaxMana(state, effects); - const currentMana = state.rawMana; - const incursionStrength = state.incursionStrength || 0; - - // Apply incursion penalty - regen *= (1 - incursionStrength); - - return regen; -} - -export function computeClickMana( - state: Pick, - effects?: ComputedEffects | UnifiedEffects -): number { - const base = - 1 + - (state.skills.manaTap || 0) * 1 + - (state.skills.manaSurge || 0) * 3; - - // If effects not provided, compute unified effects (includes equipment) - if (!effects && state.equipmentInstances && state.equippedInstances) { - effects = getUnifiedEffects(state as any); - } - - // Apply effects if available (now includes equipment bonuses) - if (effects) { - return Math.floor((base + effects.clickManaBonus) * effects.clickManaMultiplier); - } - return base; -} +// ─── Local Helper Functions ──────────────────────────────────────────────────── // Elemental damage bonus: +50% if spell element opposes floor element (super effective) // -25% if spell element matches its own opposite (weak) @@ -288,129 +109,6 @@ function getElementalBonus(spellElem: string, floorElem: string): number { return 1.0; // Neutral } -export function calcDamage( - state: Pick, - spellId: string, - floorElem?: string -): number { - const sp = SPELLS_DEF[spellId]; - if (!sp) return 5; - const skills = state.skills; - const baseDmg = sp.dmg + (skills.combatTrain || 0) * 5; - const pct = 1 + (skills.arcaneFury || 0) * 0.1; - - // Elemental mastery bonus - const elemMasteryBonus = 1 + (skills.elementalMastery || 0) * 0.15; - - // Guardian bane bonus - const guardianBonus = floorElem && GUARDIANS[Object.values(GUARDIANS).find(g => g.element === floorElem)?.hp ? 0 : 0] - ? 1 + (skills.guardianBane || 0) * 0.2 - : 1; - - const critChance = (skills.precision || 0) * 0.05; - const pactMult = state.signedPacts.reduce( - (m, f) => m * (GUARDIANS[f]?.pact || 1), - 1 - ); - - let damage = baseDmg * pct * pactMult * elemMasteryBonus; - - // Apply elemental bonus if floor element provided - if (floorElem) { - damage *= getElementalBonus(sp.elem, floorElem); - } - - // Apply crit - if (Math.random() < critChance) { - damage *= 1.5; - } - - return damage; -} - -export function calcInsight(state: Pick): number { - const pu = state.prestigeUpgrades; - const skillBonus = 1 + (state.skills.insightHarvest || 0) * 0.1; - const mult = (1 + (pu.insightAmp || 0) * 0.25) * skillBonus; - return Math.floor( - (state.maxFloorReached * 15 + state.totalManaGathered / 500 + state.signedPacts.length * 150) * mult - ); -} - -// Meditation bonus now affects regen rate directly -export function getMeditationBonus(meditateTicks: number, skills: Record, 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; - - // Base meditation: ramps up over 4 hours to 1.5x - let bonus = 1 + Math.min(hours / 4, 0.5); - - // With Meditation Focus: up to 2.5x after 4 hours - if (hasMeditation && hours >= 4) { - bonus = 2.5; - } - - // With Deep Trance: up to 3.0x after 6 hours - if (hasDeepTrance && hours >= 6) { - bonus = 3.0; - } - - // With Void Meditation: up to 5.0x after 8 hours - if (hasVoidMeditation && hours >= 8) { - bonus = 5.0; - } - - // Apply meditation efficiency from upgrades (Deep Wellspring, etc.) - bonus *= meditationEfficiency; - - return bonus; -} - -export function getIncursionStrength(day: number, hour: number): number { - if (day < INCURSION_START_DAY) return 0; - const totalHours = (day - INCURSION_START_DAY) * 24 + hour; - const maxHours = (MAX_DAY - INCURSION_START_DAY) * 24; - return Math.min(0.95, (totalHours / maxHours) * 0.95); -} - -// Check if player can afford spell cost -export function canAffordSpellCost( - cost: SpellCost, - rawMana: number, - elements: Record -): boolean { - if (cost.type === 'raw') { - return rawMana >= cost.amount; - } else { - const elem = elements[cost.element || '']; - return elem && elem.unlocked && elem.current >= cost.amount; - } -} - -// Deduct spell cost from appropriate mana pool -function deductSpellCost( - cost: SpellCost, - rawMana: number, - elements: Record -): { rawMana: number; elements: Record } { - const newElements = { ...elements }; - - if (cost.type === 'raw') { - return { rawMana: rawMana - cost.amount, elements: newElements }; - } else if (cost.element && newElements[cost.element]) { - newElements[cost.element] = { - ...newElements[cost.element], - current: newElements[cost.element].current - cost.amount - }; - return { rawMana, elements: newElements }; - } - - return { rawMana, elements: newElements }; -} - // ─── Initial State Factory ──────────────────────────────────────────────────── function makeInitial(overrides: Partial = {}): GameState { @@ -587,17 +285,12 @@ function makeInitial(overrides: Partial = {}): GameState { // ─── Game Store ─────────────────────────────────────────────────────────────── -interface GameStore extends GameState, CraftingActions, FamiliarActions { +interface GameStore extends GameState, CraftingActions, FamiliarActions, NavigationActions, StudyActions { // Actions tick: () => void; gatherMana: () => void; setAction: (action: GameAction) => void; setSpell: (spellId: string) => void; - startStudyingSkill: (skillId: string) => void; - startStudyingSpell: (spellId: string) => void; - startParallelStudySkill: (skillId: string) => void; - cancelStudy: () => void; - cancelParallelStudy: () => void; convertMana: (element: string, amount: number) => void; unlockElement: (element: string) => void; craftComposite: (target: string) => void; @@ -611,10 +304,6 @@ interface GameStore extends GameState, CraftingActions, FamiliarActions { commitSkillUpgrades: (skillId: string, upgradeIds: string[]) => void; tierUpSkill: (skillId: string) => void; - // Floor Navigation - setClimbDirection: (direction: 'up' | 'down') => void; - changeFloor: (direction: 'up' | 'down') => void; - // Inventory Management updateLootInventory: (inventory: LootInventory) => void; @@ -633,6 +322,8 @@ export const useGameStore = create()( (set, get) => ({ ...makeInitial(), ...createFamiliarSlice(set, get), + ...createNavigationSlice(set, get), + ...createStudySlice(set, get), getMaxMana: () => computeMaxMana(get()), getRegen: () => computeRegen(get()), @@ -1221,108 +912,6 @@ export const useGameStore = create()( } }, - 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; - } - } - - // Check mana cost (with focused mind reduction) - const costMult = getStudyCostMultiplier(state.skills); - const cost = Math.floor(sk.base * (currentLevel + 1) * costMult); - if (state.rawMana < cost) return; - - // Start studying - set({ - rawMana: state.rawMana - cost, - currentAction: 'study', - currentStudyTarget: { - type: 'skill', - id: skillId, - progress: state.skillProgress[skillId] || 0, - required: sk.studyTime, - }, - log: [`📚 Started studying ${sk.name}...`, ...state.log.slice(0, 49)], - }); - }, - - startStudyingSpell: (spellId: string) => { - const state = get(); - const sp = SPELLS_DEF[spellId]; - if (!sp || state.spells[spellId]?.learned) return; - - // Check mana cost (with focused mind reduction) - const costMult = getStudyCostMultiplier(state.skills); - const cost = Math.floor(sp.unlock * costMult); - if (state.rawMana < cost) return; - - const studyTime = sp.studyTime || (sp.tier * 4); // Default study time based on tier - - // Start studying - set({ - rawMana: state.rawMana - cost, - currentAction: 'study', - currentStudyTarget: { - type: 'spell', - id: spellId, - progress: state.spells[spellId]?.studyProgress || 0, - required: studyTime, - }, - spells: { - ...state.spells, - [spellId]: { ...(state.spells[spellId] || { learned: false, level: 0 }), studyProgress: state.spells[spellId]?.studyProgress || 0 }, - }, - log: [`📚 Started studying ${sp.name}...`, ...state.log.slice(0, 49)], - }); - }, - - 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)], - }); - } - }, - convertMana: (element: string, amount: number = 1) => { const state = get(); const e = state.elements[element]; @@ -1527,42 +1116,7 @@ export const useGameStore = create()( log: [`🌟 ${SKILLS_DEF[baseSkillId]?.name || baseSkillId} evolved to Tier ${nextTier}!`, ...state.log.slice(0, 49)], }); }, - - 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; - - set({ - parallelStudyTarget: { - type: 'skill', - id: skillId, - progress: state.skillProgress[skillId] || 0, - required: sk.studyTime, - }, - log: [`📚 Started parallel study of ${sk.name}... (50% speed)`, ...state.log.slice(0, 49)], - }); - }, - - cancelParallelStudy: () => { - set((state) => { - if (!state.parallelStudyTarget) return state; - return { - parallelStudyTarget: null, - log: ['📖 Parallel study cancelled.', ...state.log.slice(0, 49)], - }; - }); - }, - + getSkillUpgradeChoices: (skillId: string, milestone: 5 | 10) => { const state = get(); const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId; @@ -2007,47 +1561,6 @@ export const useGameStore = create()( }; }); }, - - // ─── Floor Navigation ──────────────────────────────────────────────────────── - - setClimbDirection: (direction: 'up' | 'down') => { - set({ climbDirection: direction }); - }, - - 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)], - }); - }, }), { name: 'mana-loop-storage', diff --git a/src/lib/game/study-slice.ts b/src/lib/game/study-slice.ts new file mode 100644 index 0000000..7f817e0 --- /dev/null +++ b/src/lib/game/study-slice.ts @@ -0,0 +1,166 @@ +// ─── Study Slice ───────────────────────────────────────────────────────────── +// Actions for studying skills and spells + +import type { GameState } from './types'; +import { SKILLS_DEF, SPELLS_DEF, getStudyCostMultiplier } from './constants'; + +// ─── 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 | ((state: GameState) => Partial)) => void, + get: () => GameState +): StudyActions { + return { + // Start studying a skill + 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; + } + } + + // Check mana cost (with focused mind reduction) + const costMult = getStudyCostMultiplier(state.skills); + const cost = Math.floor(sk.base * (currentLevel + 1) * costMult); + if (state.rawMana < cost) return; + + // Start studying + set({ + rawMana: state.rawMana - cost, + currentAction: 'study', + currentStudyTarget: { + type: 'skill', + id: skillId, + progress: state.skillProgress[skillId] || 0, + required: sk.studyTime, + }, + log: [`📚 Started studying ${sk.name}...`, ...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; + + // Check mana cost (with focused mind reduction) + const costMult = getStudyCostMultiplier(state.skills); + const cost = Math.floor(sp.unlock * costMult); + if (state.rawMana < cost) return; + + const studyTime = sp.studyTime || (sp.tier * 4); // Default study time based on tier + + // Start studying + set({ + rawMana: state.rawMana - cost, + currentAction: 'study', + currentStudyTarget: { + type: 'spell', + id: spellId, + progress: state.spells[spellId]?.studyProgress || 0, + required: studyTime, + }, + spells: { + ...state.spells, + [spellId]: { ...(state.spells[spellId] || { learned: false, level: 0 }), studyProgress: state.spells[spellId]?.studyProgress || 0 }, + }, + log: [`📚 Started studying ${sp.name}...`, ...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; + + set({ + parallelStudyTarget: { + type: 'skill', + id: skillId, + progress: state.skillProgress[skillId] || 0, + required: sk.studyTime, + }, + 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)], + }; + }); + }, + }; +} diff --git a/worklog.md b/worklog.md index 3e110b4..fb20409 100755 --- a/worklog.md +++ b/worklog.md @@ -309,3 +309,134 @@ Stage Summary: - Multi-casting is fully functional - Game compiles and runs without errors - Lint passes with no issues + +--- +## Task ID: 3 - Component Refactoring +### Work Task +Refactor `/home/z/my-project/src/app/page.tsx` to use existing tab components and extract new components. The file was ~2500 lines with inline render functions duplicating existing tab components. + +### Work Summary +**Components Created:** +1. `ActionButtons.tsx` - Extracted from `renderActionButtons()`, handles main action buttons (Meditate, Climb, Study, Convert) and crafting action buttons +2. `CalendarDisplay.tsx` - Extracted from `renderCalendar()`, renders the day calendar with incursion indicators +3. `CraftingProgress.tsx` - Extracted from `renderCraftingProgress()`, shows design/preparation/application progress bars +4. `StudyProgress.tsx` - Extracted from `renderStudyProgress()`, displays current study progress with cancel button +5. `ManaDisplay.tsx` - New component for mana/gathering section with progress bar and gather button +6. `TimeDisplay.tsx` - New component for day/hour display with pause toggle + +**Tab Components Updated:** +- `SpireTab.tsx` - Updated to include all functionality from inline version: + - Multi-spell support with activeEquipmentSpells + - Individual cast progress bars for each spell + - DPS calculation for multiple spells + - Parallel study support + - Crafting progress display + - ComboMeter integration + - Known Spells display + - Activity Log + +**File Changes:** +- `page.tsx` reduced from ~2555 lines to 1695 lines (34% reduction) +- Removed inline render functions: `renderCalendar`, `renderActionButtons`, `renderCraftingProgress`, `renderStudyProgress`, `renderSpireTab`, `renderSpellsTab`, `renderLabTab` +- Updated imports to use extracted components +- Cleaned up unused imports (`MAX_DAY`, `INCURSION_START_DAY`, `MANA_PER_ELEMENT`) +- Created `tabs/index.ts` for cleaner tab component exports +- Updated `game/index.ts` to export all new components + +**Results:** +- All lint checks pass +- Functionality preserved - all features working as before +- Better code organization with reusable components +- Easier maintenance with separated concerns + +--- +## Task ID: 1 - Code Extraction +### Work Task +Extract computed stats and utility functions from `/home/z/my-project/src/lib/game/store.ts` into a new file `/home/z/my-project/src/lib/game/computed-stats.ts`. The store.ts was ~2100 lines with functions that could be better organized in a separate module. + +### Work Summary +**Created New File:** `computed-stats.ts` + +**Functions Extracted:** +1. `DEFAULT_EFFECTS` constant - Default empty effects object for computed effects +2. `fmt` and `fmtDec` - Number formatting utilities (K, M, B suffixes) +3. `getFloorMaxHP` - Floor HP calculation with guardian and scaling logic +4. `getFloorElement` - Floor element determination from cycle +5. `getActiveEquipmentSpells` - Helper to get all spells from equipped caster weapons +6. `getEffectiveSkillLevel` - Helper for tiered skill level calculation +7. `computeMaxMana` - Maximum mana calculation with effects +8. `computeElementMax` - Elemental mana capacity calculation +9. `computeRegen` - Mana regeneration rate calculation +10. `computeEffectiveRegen` - Regen with dynamic special effects +11. `computeClickMana` - Click mana gain calculation +12. `getElementalBonus` - Elemental damage bonus helper +13. `calcDamage` - Damage calculation with skills, pacts, and crits +14. `calcInsight` - End-of-loop insight calculation +15. `getMeditationBonus` - Meditation multiplier calculation +16. `getIncursionStrength` - Time-based incursion penalty +17. `canAffordSpellCost` - Check if player can afford spell +18. `deductSpellCost` - Deduct spell cost from mana pool + +**Files Updated:** +- `store.ts`: + - Added imports from computed-stats.ts + - Removed duplicate function definitions + - Kept local `getElementalBonus` helper (used only in store) + - Re-exports `fmt` and `fmtDec` for backward compatibility +- `formatting.ts`: + - Added re-exports of `fmt` and `fmtDec` from computed-stats.ts + +**Results:** +- All lint checks pass +- No functionality changed - pure refactoring +- Better code organization with computed stats in dedicated module +- Easier to test and maintain individual functions +- Backward compatible - existing imports still work + +--- +## Task ID: 2 - Store Slice Refactoring +### Work Task +Create two new store slices to reduce the size of `/home/z/my-project/src/lib/game/store.ts`: +1. `navigation-slice.ts` - Extract navigation-related actions +2. `study-slice.ts` - Extract study-related actions + +### Work Summary +**Created New Files:** + +**1. `navigation-slice.ts`** - Floor navigation actions: +- `NavigationActions` interface defining the action types +- `createNavigationSlice()` factory function +- `setClimbDirection()` - Set climbing direction (up/down) +- `changeFloor()` - Manually change floors with respawn logic + +**2. `study-slice.ts`** - Study system actions: +- `StudyActions` interface defining the action types +- `createStudySlice()` factory function +- `startStudyingSkill()` - Begin studying a skill with prerequisite and cost checks +- `startStudyingSpell()` - Begin studying a spell with cost checks +- `cancelStudy()` - Cancel current study with progress retention bonus +- `startParallelStudySkill()` - Start parallel study (requires Parallel Mind upgrade) +- `cancelParallelStudy()` - Cancel parallel study + +**Files Updated:** +- `store.ts`: + - Added imports for `createNavigationSlice` and `createStudySlice` + - Added imports for `NavigationActions` and `StudyActions` interfaces + - Updated `GameStore` interface to extend both new action interfaces + - Spread the new slices into the store + - Removed duplicated action implementations + - Added re-exports for computed stats functions (`getFloorElement`, `computeMaxMana`, `computeRegen`, `computeClickMana`, `calcDamage`, `getMeditationBonus`, `getIncursionStrength`, `canAffordSpellCost`, `getFloorMaxHP`) + - Added missing imports for `EQUIPMENT_TYPES` and `EquipmentInstance` + +**Pattern Followed:** +- Followed existing slice patterns from `familiar-slice.ts` and `crafting-slice.ts` +- Used factory function pattern that accepts `set` and `get` from Zustand +- Exported both the interface and factory function +- Proper TypeScript typing throughout + +**Results:** +- All lint checks pass +- No functionality changed - pure refactoring +- Reduced store.ts size by extracting ~100 lines of action implementations +- Better code organization with navigation and study logic in dedicated modules +- Easier to maintain and extend individual features