'use client'; 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 { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution'; import { getUnifiedEffects, hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects'; import type { SkillUpgradeChoice } from '@/lib/game/types'; import { formatSpellCost, getSpellCostColor, formatStudyTime, formatHour } from '@/lib/game/formatting'; import { Button } from '@/components/ui/button'; import { Progress } from '@/components/ui/progress'; 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 { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; 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 } from 'lucide-react'; import type { GameAction } from '@/lib/game/types'; import { CraftingTab } from '@/components/game/tabs/CraftingTab'; import { FamiliarTab } from '@/components/game/tabs/FamiliarTab'; import { ComboMeter } from '@/components/game/ComboMeter'; import { LootInventoryDisplay } from '@/components/game/LootInventory'; import { AchievementsDisplay } from '@/components/game/AchievementsDisplay'; import { ACHIEVEMENTS } from '@/lib/game/data/achievements'; // 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; } // Element icon mapping const ELEMENT_ICONS: Record = { fire: Flame, water: Droplet, air: Wind, earth: Mountain, light: Sun, dark: Moon, life: Leaf, death: Skull, mental: Brain, transference: Link, force: Force, blood: Droplets, metal: Target, wood: TreeDeciduous, sand: Hourglass, crystal: Gem, stellar: Star, void: CircleDot, raw: Circle, }; export default function ManaLoopGame() { const [activeTab, setActiveTab] = useState('spire'); const [convertTarget, setConvertTarget] = useState('fire'); const [isGathering, setIsGathering] = useState(false); const [upgradeDialogSkill, setUpgradeDialogSkill] = useState(null); const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5); const [pendingUpgradeSelections, setPendingUpgradeSelections] = useState([]); // Game store const store = useGameStore(); const gameLoop = useGameLoop(); // Computed effects from upgrades and equipment (must be before other derived stats) const upgradeEffects = getUnifiedEffects(store); // Derived stats const maxMana = computeMaxMana(store, upgradeEffects); const baseRegen = computeRegen(store, upgradeEffects); const clickMana = computeClickMana(store); 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 meditationMultiplier = getMeditationBonus(store.meditateTicks, store.skills, upgradeEffects.meditationEfficiency); const incursionStrength = getIncursionStrength(store.day, store.hour); const studySpeedMult = getStudySpeedMultiplier(store.skills); const studyCostMult = getStudyCostMultiplier(store.skills); // Effective regen with incursion penalty and upgrade effects // Note: baseRegen already includes upgradeEffects.regenBonus and upgradeEffects.regenMultiplier const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength); // Mana Cascade bonus: +0.1 regen per 100 max mana const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE) ? Math.floor(maxMana / 100) * 0.1 : 0; // Auto-gather while holding - using requestAnimationFrame for smoother performance useEffect(() => { if (!isGathering) return; let lastGatherTime = 0; const minGatherInterval = 100; // Minimum 100ms between gathers (10 times per second max) let animationFrameId: number; const gatherLoop = (timestamp: number) => { if (timestamp - lastGatherTime >= minGatherInterval) { store.gatherMana(); lastGatherTime = timestamp; } animationFrameId = requestAnimationFrame(gatherLoop); }; animationFrameId = requestAnimationFrame(gatherLoop); return () => cancelAnimationFrame(animationFrameId); }, [isGathering, store]); // Handle gather button mouse/touch events const handleGatherStart = () => { setIsGathering(true); store.gatherMana(); // Immediate first click }; const handleGatherEnd = () => { setIsGathering(false); }; // Damage breakdown const getDamageBreakdown = () => { const spell = SPELLS_DEF[store.activeSpell]; if (!spell) return null; const baseDmg = spell.dmg; const combatTrainBonus = (store.skills.combatTrain || 0) * 5; const arcaneFuryMult = 1 + (store.skills.arcaneFury || 0) * 0.1; const elemMasteryMult = 1 + (store.skills.elementalMastery || 0) * 0.15; const guardianBaneMult = isGuardianFloor ? (1 + (store.skills.guardianBane || 0) * 0.2) : 1; const pactMult = store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1); const precisionChance = (store.skills.precision || 0) * 0.05; // Elemental bonus let elemBonus = 1.0; let elemBonusText = ''; if (spell.elem !== 'raw' && floorElem) { if (spell.elem === floorElem) { elemBonus = 1.25; elemBonusText = '+25% same element'; } else if (ELEMENTS[spell.elem] && ELEMENT_OPPOSITES[floorElem] === spell.elem) { elemBonus = 1.5; elemBonusText = '+50% super effective'; } } return { base: baseDmg, combatTrainBonus, arcaneFuryMult, elemMasteryMult, guardianBaneMult, pactMult, precisionChance, elemBonus, elemBonusText, total: calcDamage(store, store.activeSpell, floorElem) }; }; const damageBreakdown = getDamageBreakdown(); // Get all active spells from equipment const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances); // Compute DPS based on cast speed for ALL active spells const getTotalDPS = () => { const quickCastBonus = 1 + (store.skills.quickCast || 0) * 0.05; const attackSpeedMult = upgradeEffects.attackSpeedMultiplier; const castsPerSecondMult = HOURS_PER_TICK / (TICK_MS / 1000); let totalDPS = 0; for (const { spellId } of activeEquipmentSpells) { const spell = SPELLS_DEF[spellId]; if (!spell) continue; const spellCastSpeed = spell.castSpeed || 1; const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult; const damagePerCast = calcDamage(store, spellId, floorElem); const castsPerSecond = totalCastSpeed * castsPerSecondMult; totalDPS += damagePerCast * castsPerSecond; } return totalDPS; }; const totalDPS = getTotalDPS(); // Effective regen (with meditation, incursion, cascade) // Note: baseRegen already includes upgradeEffects multipliers and bonuses const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus) * meditationMultiplier; // Start game loop useEffect(() => { const cleanup = gameLoop.start(); return cleanup; }, [gameLoop]); // 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]; if (!spell) return false; 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) : '—'}
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 => { // skillId should already be the tiered skill ID if applicable const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId; const path = SKILL_EVOLUTION_PATHS[baseSkillId]; if (!path) return null; // Check level 5 milestone if (level >= 5) { const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, store.skillTiers); const selected5 = (store.skillUpgrades[skillId] || []).filter(id => id.includes('_l5')); // Can select up to 2 upgrades per milestone if (upgrades5.length > 0 && selected5.length < 2) { return { milestone: 5, hasUpgrades: true, selectedCount: selected5.length }; } } // Check level 10 milestone if (level >= 10) { const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, store.skillTiers); const selected10 = (store.skillUpgrades[skillId] || []).filter(id => id.includes('_l10')); if (upgrades10.length > 0 && selected10.length < 2) { return { milestone: 10, hasUpgrades: true, selectedCount: selected10.length }; } } return null; }; // Render upgrade selection dialog const renderUpgradeDialog = () => { if (!upgradeDialogSkill) return null; const skillDef = SKILLS_DEF[upgradeDialogSkill]; const level = store.skills[upgradeDialogSkill] || 0; const { available, selected: alreadySelected } = store.getSkillUpgradeChoices(upgradeDialogSkill, upgradeDialogMilestone); // Use pending selections or fall back to already selected const currentSelections = pendingUpgradeSelections.length > 0 ? pendingUpgradeSelections : alreadySelected; // Toggle selection const toggleUpgrade = (upgradeId: string) => { if (currentSelections.includes(upgradeId)) { setPendingUpgradeSelections(currentSelections.filter(id => id !== upgradeId)); } else if (currentSelections.length < 2) { setPendingUpgradeSelections([...currentSelections, upgradeId]); } }; // Commit selections and close const handleDone = () => { if (currentSelections.length === 2 && upgradeDialogSkill) { store.commitSkillUpgrades(upgradeDialogSkill, currentSelections); } setPendingUpgradeSelections([]); setUpgradeDialogSkill(null); }; // Cancel and close const handleCancel = () => { setPendingUpgradeSelections([]); setUpgradeDialogSkill(null); }; return ( { if (!open) { setPendingUpgradeSelections([]); setUpgradeDialogSkill(null); } }}> Choose Upgrade - {skillDef?.name || upgradeDialogSkill} Level {upgradeDialogMilestone} Milestone - Select 2 upgrades ({currentSelections.length}/2 chosen)
{available.map((upgrade) => { const isSelected = currentSelections.includes(upgrade.id); const canToggle = currentSelections.length < 2 || isSelected; return (
{ if (canToggle) { toggleUpgrade(upgrade.id); } }} >
{upgrade.name}
{isSelected && Selected}
{upgrade.desc}
{upgrade.effect.type === 'multiplier' && (
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
)} {upgrade.effect.type === 'bonus' && (
+{upgrade.effect.value} {upgrade.effect.stat}
)} {upgrade.effect.type === 'special' && (
⚡ {upgrade.effect.specialDesc || 'Special effect'}
)}
); })}
); }; // Skills Tab const renderSkillsTab = () => (
{/* Upgrade Selection Dialog */} {renderUpgradeDialog()} {/* Current Study Progress */} {store.currentStudyTarget && store.currentStudyTarget.type === 'skill' && ( {renderStudyProgress()} )} {SKILL_CATEGORIES.map((cat) => { const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id); if (skillsInCat.length === 0) return null; return ( {cat.icon} {cat.name}
{skillsInCat.map(([id, def]) => { // Get tier info - check if this skill has evolved const currentTier = store.skillTiers?.[id] || 1; const tieredSkillId = currentTier > 1 ? `${id}_t${currentTier}` : id; const tierMultiplier = getTierMultiplier(tieredSkillId); // Get the actual level from the tiered skill const level = store.skills[tieredSkillId] || store.skills[id] || 0; const maxed = level >= def.max; // Check if studying this skill (either base or tiered) const isStudying = (store.currentStudyTarget?.id === id || store.currentStudyTarget?.id === tieredSkillId) && store.currentStudyTarget?.type === 'skill'; const savedProgress = store.skillProgress[tieredSkillId] || store.skillProgress[id] || 0; // Get tier name for display const tierDef = SKILL_EVOLUTION_PATHS[id]?.tiers.find(t => t.tier === currentTier); const skillDisplayName = tierDef?.name || def.name; // Check prerequisites let prereqMet = true; if (def.req) { for (const [r, rl] of Object.entries(def.req)) { if ((store.skills[r] || 0) < rl) { prereqMet = false; break; } } } // Apply skill modifiers const costMult = getStudyCostMultiplier(store.skills); const speedMult = getStudySpeedMultiplier(store.skills); const studyEffects = getUnifiedEffects(store); const effectiveSpeedMult = speedMult * studyEffects.studySpeedMultiplier; // Study time scales with tier const tierStudyTime = def.studyTime * currentTier; const effectiveStudyTime = tierStudyTime / effectiveSpeedMult; // Cost scales with tier const baseCost = def.base * (level + 1) * currentTier; const cost = Math.floor(baseCost * costMult); // Can start studying? (study the tiered skill) const canStudy = !maxed && prereqMet && store.rawMana >= cost && !isStudying; // Check for milestone upgrades (use tiered skill ID) const milestoneInfo = hasMilestoneUpgrade(tieredSkillId, level); // Check for tier up const nextTierSkill = getNextTierSkill(tieredSkillId); const canTierUp = maxed && nextTierSkill; // Get selected upgrades for this skill (from tiered skill) const selectedUpgrades = store.skillUpgrades[tieredSkillId] || []; const selectedL5 = selectedUpgrades.filter(u => u.includes('_l5')); const selectedL10 = selectedUpgrades.filter(u => u.includes('_l10')); return (
{skillDisplayName} {/* Show tier */} {currentTier > 1 && ( Tier {currentTier} ({fmtDec(tierMultiplier, 0)}x) )} {level > 0 && Lv.{level}} {/* Show selected upgrades */} {selectedUpgrades.length > 0 && (
{selectedL5.length > 0 && ( L5: {selectedL5.length} )} {selectedL10.length > 0 && ( L10: {selectedL10.length} )}
)}
{def.desc}{currentTier > 1 && ` (Tier ${currentTier}: ${fmtDec(tierMultiplier, 0)}x effect)`}
{!prereqMet && def.req && (
Requires: {Object.entries(def.req).map(([r, rl]) => `${SKILLS_DEF[r]?.name} Lv.${rl}`).join(', ')}
)}
1 ? 'text-green-400' : ''}> Study: {formatStudyTime(effectiveStudyTime)}{effectiveSpeedMult > 1 && ({Math.round(effectiveSpeedMult * 100)}% speed)} {' • '} Cost: {fmt(cost)} mana{costMult < 1 && ({Math.round(costMult * 100)}% cost)}
{/* Milestone indicator */} {milestoneInfo && (
⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
)}
{/* Level dots */}
{Array.from({ length: def.max }).map((_, i) => (
))}
{isStudying ? (
{formatStudyTime(store.currentStudyTarget?.progress || 0)}/{formatStudyTime(tierStudyTime)}
) : milestoneInfo ? ( ) : canTierUp ? ( ) : maxed ? ( Maxed ) : (
{/* Parallel Study button */} {hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) && store.currentStudyTarget && !store.parallelStudyTarget && store.currentStudyTarget.id !== tieredSkillId && canStudy && (

Study in parallel (50% speed)

)}
)}
); })}
); })}
); // Stats Tab - Comprehensive stats overview const renderStatsTab = () => { // Compute element max const elemMax = (() => { const ea = store.skillTiers?.elemAttune || 1; const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune'; const level = store.skills[tieredSkillId] || store.skills.elemAttune || 0; const tierMult = getTierMultiplier(tieredSkillId); return 10 + level * 50 * tierMult + (store.prestigeUpgrades.elementalAttune || 0) * 25; })(); // Get all selected skill upgrades const getAllSelectedUpgrades = () => { const upgrades: { skillId: string; upgrade: SkillUpgradeChoice }[] = []; for (const [skillId, selectedIds] of Object.entries(store.skillUpgrades)) { const baseSkillId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId; const path = SKILL_EVOLUTION_PATHS[baseSkillId]; if (!path) continue; for (const tier of path.tiers) { if (tier.skillId === skillId) { for (const upgradeId of selectedIds) { const upgrade = tier.upgrades.find(u => u.id === upgradeId); if (upgrade) { upgrades.push({ skillId, upgrade }); } } } } } return upgrades; }; const selectedUpgrades = getAllSelectedUpgrades(); return (
{/* Mana Stats */} Mana Stats
Base Max Mana: 100
Mana Well Bonus: {(() => { const mw = store.skillTiers?.manaWell || 1; const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell'; const level = store.skills[tieredSkillId] || store.skills.manaWell || 0; const tierMult = getTierMultiplier(tieredSkillId); return `+${fmt(level * 100 * tierMult)} (${level} lvl × 100 × ${tierMult}x tier)`; })()}
Prestige Mana Well: +{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}
{/* Skill Upgrade Effects on Max Mana */} {upgradeEffects.maxManaBonus > 0 && (
Upgrade Mana Bonus: +{fmt(upgradeEffects.maxManaBonus)}
)} {upgradeEffects.maxManaMultiplier > 1 && (
Upgrade Mana Multiplier: ×{fmtDec(upgradeEffects.maxManaMultiplier, 2)}
)}
Total Max Mana: {fmt(maxMana)}
Base Regen: 2/hr
Mana Flow Bonus: {(() => { const mf = store.skillTiers?.manaFlow || 1; const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow'; const level = store.skills[tieredSkillId] || store.skills.manaFlow || 0; const tierMult = getTierMultiplier(tieredSkillId); return `+${fmtDec(level * 1 * tierMult)}/hr (${level} lvl × 1 × ${tierMult}x tier)`; })()}
Mana Spring Bonus: +{(store.skills.manaSpring || 0) * 2}/hr
Prestige Mana Flow: +{fmtDec((store.prestigeUpgrades.manaFlow || 0) * 0.5)}/hr
Temporal Echo: ×{fmtDec(1 + (store.prestigeUpgrades.temporalEcho || 0) * 0.1, 2)}
Base Regen: {fmtDec(baseRegen, 2)}/hr
{/* Skill Upgrade Effects on Regen */} {upgradeEffects.regenBonus > 0 && (
Upgrade Regen Bonus: +{fmtDec(upgradeEffects.regenBonus, 2)}/hr
)} {upgradeEffects.permanentRegenBonus > 0 && (
Permanent Regen Bonus: +{fmtDec(upgradeEffects.permanentRegenBonus, 2)}/hr
)} {upgradeEffects.regenMultiplier > 1 && (
Upgrade Regen Multiplier: ×{fmtDec(upgradeEffects.regenMultiplier, 2)}
)}
{/* Skill Upgrade Effects Summary */} {upgradeEffects.activeUpgrades.length > 0 && ( <>
Active Skill Upgrades
{upgradeEffects.activeUpgrades.map((upgrade, idx) => (
{upgrade.name} {upgrade.desc}
))}
)}
Click Mana Value: +{clickMana}
Mana Tap Bonus: +{store.skills.manaTap || 0}
Mana Surge Bonus: +{(store.skills.manaSurge || 0) * 3}
Mana Overflow: ×{fmtDec(1 + (store.skills.manaOverflow || 0) * 0.25, 2)}
Meditation Multiplier: 1.5 ? 'text-purple-400' : 'text-gray-300'}`}> {fmtDec(meditationMultiplier, 2)}x
Effective Regen: {fmtDec(effectiveRegen, 2)}/hr
{incursionStrength > 0 && !hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM) && (
Incursion Penalty: -{Math.round(incursionStrength * 100)}%
)} {hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM) && incursionStrength > 0 && (
Steady Stream: Immune to incursion
)} {manaCascadeBonus > 0 && (
Mana Cascade Bonus: +{fmtDec(manaCascadeBonus, 2)}/hr
)} {hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_TORRENT) && store.rawMana > maxMana * 0.75 && (
Mana Torrent: +50% regen (high mana)
)} {hasSpecial(upgradeEffects, SPECIAL_EFFECTS.DESPERATE_WELLS) && store.rawMana < maxMana * 0.25 && (
Desperate Wells: +50% regen (low mana)
)}
{/* Combat Stats */} Combat Stats
Active Spell Base Damage: {activeSpellDef?.dmg || 5}
Combat Training Bonus: +{(store.skills.combatTrain || 0) * 5}
Arcane Fury Multiplier: ×{fmtDec(1 + (store.skills.arcaneFury || 0) * 0.1, 2)}
Elemental Mastery: ×{fmtDec(1 + (store.skills.elementalMastery || 0) * 0.15, 2)}
Guardian Bane: ×{fmtDec(1 + (store.skills.guardianBane || 0) * 0.2, 2)} (vs guardians)
Critical Hit Chance: {((store.skills.precision || 0) * 5)}%
Critical Multiplier: 1.5x
Spell Echo Chance: {((store.skills.spellEcho || 0) * 10)}%
Pact Multiplier: ×{fmtDec(store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}
Total Damage: {fmt(calcDamage(store, store.activeSpell))}
{/* Study Stats */} Study Stats
Study Speed: ×{fmtDec(studySpeedMult, 2)}
Quick Learner Bonus: +{((store.skills.quickLearner || 0) * 10)}%
Study Cost: {Math.round(studyCostMult * 100)}%
Focused Mind Bonus: -{((store.skills.focusedMind || 0) * 5)}%
Progress Retention: {Math.round((1 + (store.skills.knowledgeRetention || 0) * 0.2) * 100)}%
{/* Element Stats */} Element Stats
Element Capacity: {elemMax}
Elem. Attunement Bonus: {(() => { const ea = store.skillTiers?.elemAttune || 1; const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune'; const level = store.skills[tieredSkillId] || store.skills.elemAttune || 0; const tierMult = getTierMultiplier(tieredSkillId); return `+${level * 50 * tierMult}`; })()}
Prestige Attunement: +{(store.prestigeUpgrades.elementalAttune || 0) * 25}
Unlocked Elements: {Object.values(store.elements).filter(e => e.unlocked).length} / {Object.keys(ELEMENTS).length}
Elem. Crafting Bonus: ×{fmtDec(1 + (store.skills.elemCrafting || 0) * 0.25, 2)}
Elemental Mana Pools:
{Object.entries(store.elements) .filter(([, state]) => state.unlocked) .map(([id, state]) => { const def = ELEMENTS[id]; return (
{def?.sym}
{state.current}/{state.max}
); })}
{/* Active Upgrades */} Active Skill Upgrades ({selectedUpgrades.length}) {selectedUpgrades.length === 0 ? (
No skill upgrades selected yet. Level skills to 5 or 10 to choose upgrades.
) : (
{selectedUpgrades.map(({ skillId, upgrade }) => (
{upgrade.name} {SKILLS_DEF[skillId]?.name || skillId}
{upgrade.desc}
{upgrade.effect.type === 'multiplier' && (
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
)} {upgrade.effect.type === 'bonus' && (
+{upgrade.effect.value} {upgrade.effect.stat}
)} {upgrade.effect.type === 'special' && (
⚡ {upgrade.effect.specialDesc || 'Special effect active'}
)}
))}
)}
{/* Pact Bonuses */} Signed Pacts ({store.signedPacts.length}/10) {store.signedPacts.length === 0 ? (
No pacts signed yet. Defeat guardians to earn pacts.
) : (
{store.signedPacts.map((floor) => { const guardian = GUARDIANS[floor]; if (!guardian) return null; return (
{guardian.name}
Floor {floor}
{guardian.pact}x multiplier
); })}
Combined Pact Multiplier: ×{fmtDec(store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}
)}
{/* Loop Stats */} Loop Stats
{store.loopCount}
Loops Completed
{fmt(store.insight)}
Current Insight
{fmt(store.totalInsight)}
Total Insight
{store.maxFloorReached}
Max Floor
{Object.values(store.spells).filter(s => s.learned).length}
Spells Learned
{Object.values(store.skills).reduce((a, b) => a + b, 0)}
Total Skill Levels
{fmt(store.totalManaGathered)}
Total Mana Gathered
{store.memorySlots}
Memory Slots
); }; // Grimoire Tab (Prestige) const renderGrimoireTab = () => (
{/* Current Status */} Loop Status
{store.loopCount}
Loops Completed
{fmt(store.insight)}
Current Insight
{fmt(store.totalInsight)}
Total Insight
{store.memorySlots}
Memory Slots
{/* Signed Pacts */} Signed Pacts {store.signedPacts.length === 0 ? (
No pacts signed yet. Defeat guardians to earn pacts.
) : (
{store.signedPacts.map((floor) => { const guardian = GUARDIANS[floor]; if (!guardian) return null; return (
{guardian.name}
Floor {floor}
{guardian.pact}x multiplier
); })}
)}
{/* Prestige Upgrades */} Insight Upgrades (Permanent)
{Object.entries(PRESTIGE_DEF).map(([id, def]) => { const level = store.prestigeUpgrades[id] || 0; const maxed = level >= def.max; const canBuy = !maxed && store.insight >= def.cost; return (
{def.name}
{level}/{def.max}
{def.desc}
); })}
{/* Reset Game Button */}
Reset All Progress
Clear all data and start fresh
); // Game Over Screen if (store.gameOver) { return (
{store.victory ? '🏆 VICTORY!' : '⏰ LOOP ENDS'}

{store.victory ? 'The Awakened One falls! Your power echoes through eternity.' : 'The time loop resets... but you remember.'}

{fmt(store.loopInsight)}
Insight Gained
{store.maxFloorReached}
Best Floor
{store.signedPacts.length}
Pacts Signed
{store.loopCount + 1}
Total Loops
); } return (
{/* Header */}

MANA LOOP

Day {store.day}
{formatTime(store.hour)}
{fmt(store.insight)}
Insight
{/* Calendar */}
{renderCalendar()}
{/* Main Content */}
{/* Sidebar */} {/* Main Tab Content */}
⚔️ Spire 📖 Spells 🧪 Lab ✨ Crafting 🔮 Familiars 📚 Skills 💎 Loot 🏆 Achievements ⭐ Grimoire 📊 Stats {renderSpireTab()} {renderSpellsTab()} {renderLabTab()} {renderSkillsTab()}
Loot Information

As you climb the Spire, you'll discover valuable loot from defeated floors and guardians.

Loot Types:
  • Materials - Used for advanced crafting
  • Essence - Grants elemental mana
  • Mana Orbs - Direct mana boost
  • Blueprints - New equipment designs
Drop Rates:
  • Higher floors = better loot
  • Guardians have 2x drop rate
  • Rare drops from floor 50+
  • Legendary items from floor 75+
Loot Statistics
{Object.values(store.lootInventory.materials).reduce((a, b) => a + b, 0)}
Materials Found
{store.lootInventory.blueprints.length}
Blueprints
{store.totalSpellsCast || 0}
Spells Cast
{store.combo?.maxCombo || 0}
Max Combo
Achievement Progress
{store.achievements?.unlocked?.length || 0}
Unlocked
{(store.achievements?.unlocked || []) .reduce((sum: number, id: string) => sum + (ACHIEVEMENTS[id]?.reward?.insight || 0), 0)}
Insight Earned
Achievements provide permanent bonuses and insight rewards. Track your progress and unlock special titles!
Categories:
Combat Progression Crafting Magic Special
{renderGrimoireTab()} {renderStatsTab()}
{/* Footer */}
); }