2276 lines
104 KiB
TypeScript
Executable File
2276 lines
104 KiB
TypeScript
Executable File
'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 { 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 } from 'lucide-react';
|
||
import type { GameAction } from '@/lib/game/types';
|
||
import { CraftingTab } from '@/components/game/tabs/CraftingTab';
|
||
|
||
// Element icon mapping
|
||
const ELEMENT_ICONS: Record<string, typeof Flame> = {
|
||
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<string | null>(null);
|
||
const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5);
|
||
const [pendingUpgradeSelections, setPendingUpgradeSelections] = useState<string[]>([]);
|
||
|
||
// 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();
|
||
|
||
// Compute DPS based on cast speed
|
||
const getDPS = () => {
|
||
const spell = SPELLS_DEF[store.activeSpell];
|
||
if (!spell) return 0;
|
||
|
||
const spellCastSpeed = spell.castSpeed || 1;
|
||
const quickCastBonus = 1 + (store.skills.quickCast || 0) * 0.05;
|
||
const attackSpeedMult = upgradeEffects.attackSpeedMultiplier;
|
||
const totalCastSpeed = spellCastSpeed * quickCastBonus * attackSpeedMult;
|
||
|
||
// Damage per cast
|
||
const damagePerCast = calcDamage(store, store.activeSpell, floorElem);
|
||
|
||
// Casts per second = castSpeed / hours per second
|
||
// HOURS_PER_TICK = 0.04 hours per tick, TICK_MS = 200ms
|
||
// So castSpeed casts/hour * 0.04 hours/tick = casts per tick
|
||
// casts per tick / 0.2 seconds per tick = casts per second
|
||
const castsPerSecond = totalCastSpeed * HOURS_PER_TICK / (TICK_MS / 1000);
|
||
|
||
return damagePerCast * castsPerSecond;
|
||
};
|
||
|
||
const dps = getDPS();
|
||
|
||
// 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(
|
||
<Tooltip key={d}>
|
||
<TooltipTrigger asChild>
|
||
<div className={dayClass}>
|
||
{d}
|
||
</div>
|
||
</TooltipTrigger>
|
||
<TooltipContent>
|
||
<p>Day {d}</p>
|
||
{d >= INCURSION_START_DAY && <p className="text-red-400">Incursion Active</p>}
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
);
|
||
}
|
||
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 (
|
||
<div className="space-y-2">
|
||
<div className="grid grid-cols-2 gap-2">
|
||
{actions.map(({ id, label, icon: Icon }) => (
|
||
<Button
|
||
key={id}
|
||
variant={store.currentAction === id ? 'default' : 'outline'}
|
||
size="sm"
|
||
className={`h-9 ${store.currentAction === id ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'}`}
|
||
onClick={() => store.setAction(id)}
|
||
>
|
||
<Icon className="w-4 h-4 mr-1" />
|
||
{label}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Crafting actions row - shown when there's active crafting progress */}
|
||
{(hasDesignProgress || hasPrepProgress || hasAppProgress) && (
|
||
<div className="grid grid-cols-3 gap-2">
|
||
<Button
|
||
variant={store.currentAction === 'design' ? 'default' : 'outline'}
|
||
size="sm"
|
||
disabled={!hasDesignProgress}
|
||
className={`h-9 ${store.currentAction === 'design' ? 'bg-purple-600 hover:bg-purple-700' : 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'}`}
|
||
onClick={() => hasDesignProgress && store.setAction('design')}
|
||
>
|
||
<Target className="w-4 h-4 mr-1" />
|
||
Design
|
||
</Button>
|
||
<Button
|
||
variant={store.currentAction === 'prepare' ? 'default' : 'outline'}
|
||
size="sm"
|
||
disabled={!hasPrepProgress}
|
||
className={`h-9 ${store.currentAction === 'prepare' ? 'bg-purple-600 hover:bg-purple-700' : 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'}`}
|
||
onClick={() => hasPrepProgress && store.setAction('prepare')}
|
||
>
|
||
<FlaskConical className="w-4 h-4 mr-1" />
|
||
Prepare
|
||
</Button>
|
||
<Button
|
||
variant={store.currentAction === 'enchant' ? 'default' : 'outline'}
|
||
size="sm"
|
||
disabled={!hasAppProgress}
|
||
className={`h-9 ${store.currentAction === 'enchant' ? 'bg-purple-600 hover:bg-purple-700' : 'bg-gray-800/50 hover:bg-gray-700/50 border-gray-600'}`}
|
||
onClick={() => hasAppProgress && store.setAction('enchant')}
|
||
>
|
||
<Sparkles className="w-4 h-4 mr-1" />
|
||
Enchant
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 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(
|
||
<div key="design" className="p-3 rounded border border-cyan-600/50 bg-cyan-900/20">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
<Target className="w-4 h-4 text-cyan-400" />
|
||
<span className="text-sm font-semibold text-cyan-300">
|
||
Designing Enchantment
|
||
</span>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
||
onClick={() => store.cancelDesign?.()}
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||
<span>{formatStudyTime(store.designProgress.progress)} / {formatStudyTime(store.designProgress.required)}</span>
|
||
<span>Design Time</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 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(
|
||
<div key="prepare" className="p-3 rounded border border-green-600/50 bg-green-900/20">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
<FlaskConical className="w-4 h-4 text-green-400" />
|
||
<span className="text-sm font-semibold text-green-300">
|
||
Preparing {instance?.name || 'Equipment'}
|
||
</span>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
||
onClick={() => store.cancelPreparation?.()}
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||
<span>{formatStudyTime(store.preparationProgress.progress)} / {formatStudyTime(store.preparationProgress.required)}</span>
|
||
<span>Mana spent: {fmt(store.preparationProgress.manaCostPaid)}</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 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(
|
||
<div key="enchant" className="p-3 rounded border border-amber-600/50 bg-amber-900/20">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
<Sparkles className="w-4 h-4 text-amber-400" />
|
||
<span className="text-sm font-semibold text-amber-300">
|
||
Enchanting {instance?.name || 'Equipment'}
|
||
</span>
|
||
</div>
|
||
<div className="flex gap-1">
|
||
{store.applicationProgress.paused ? (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0 text-green-400 hover:text-green-300"
|
||
onClick={() => store.resumeApplication?.()}
|
||
>
|
||
<Play className="w-4 h-4" />
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0 text-yellow-400 hover:text-yellow-300"
|
||
onClick={() => store.pauseApplication?.()}
|
||
>
|
||
<Pause className="w-4 h-4" />
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
||
onClick={() => store.cancelApplication?.()}
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||
<span>{formatStudyTime(store.applicationProgress.progress)} / {formatStudyTime(store.applicationProgress.required)}</span>
|
||
<span>Mana/hr: {fmt(store.applicationProgress.manaPerHour)}</span>
|
||
</div>
|
||
{design && (
|
||
<div className="text-xs text-amber-400/70 mt-1">
|
||
Applying: {design.name}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return progressSections.length > 0 ? (
|
||
<div className="space-y-2">
|
||
{progressSections}
|
||
</div>
|
||
) : 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 (
|
||
<div className="p-3 rounded border border-purple-600/50 bg-purple-900/20">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
<BookOpen className="w-4 h-4 text-purple-400" />
|
||
<span className="text-sm font-semibold text-purple-300">
|
||
{def?.name}
|
||
{isSkill && ` Lv.${currentLevel + 1}`}
|
||
</span>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
||
onClick={() => store.cancelStudy()}
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||
<span>{formatStudyTime(target.progress)} / {formatStudyTime(target.required)}</span>
|
||
<span>{studySpeedMult.toFixed(1)}x speed</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 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 = () => (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
{/* Current Floor Card */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Current Floor</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<div className="flex items-baseline gap-2">
|
||
<span className="text-4xl font-bold game-title" style={{ color: floorElemDef?.color }}>
|
||
{store.currentFloor}
|
||
</span>
|
||
<span className="text-gray-400 text-sm">/ 100</span>
|
||
<span className="ml-auto text-sm" style={{ color: floorElemDef?.color }}>
|
||
{floorElemDef?.sym} {floorElemDef?.name}
|
||
</span>
|
||
{isGuardianFloor && (
|
||
<Badge className="bg-red-900/50 text-red-300 border-red-600">GUARDIAN</Badge>
|
||
)}
|
||
</div>
|
||
|
||
{isGuardianFloor && currentGuardian && (
|
||
<div className="text-sm font-semibold game-panel-title" style={{ color: floorElemDef?.color }}>
|
||
⚔️ {currentGuardian.name}
|
||
</div>
|
||
)}
|
||
|
||
{/* HP Bar */}
|
||
<div className="space-y-1">
|
||
<div className="h-3 bg-gray-800 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full rounded-full transition-all duration-300"
|
||
style={{
|
||
width: `${Math.max(0, (store.floorHP / store.floorMaxHP) * 100)}%`,
|
||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
||
boxShadow: `0 0 10px ${floorElemDef?.glow}`,
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
||
<span>{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP</span>
|
||
<span>DPS: {store.currentAction === 'climb' && canCastSpell(store.activeSpell) ? fmtDec(dps) : '—'}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<Separator className="bg-gray-700" />
|
||
|
||
<div className="text-sm text-gray-400">
|
||
Best: Floor <strong className="text-gray-200">{store.maxFloorReached}</strong> •
|
||
Pacts: <strong className="text-amber-400">{store.signedPacts.length}</strong>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Active Spell Card */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Active Spell</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{activeSpellDef ? (
|
||
<>
|
||
<div className="text-lg font-semibold game-panel-title" style={{ color: activeSpellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[activeSpellDef.elem]?.color }}>
|
||
{activeSpellDef.name}
|
||
{activeSpellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200">Basic</Badge>}
|
||
{activeSpellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100">Legendary</Badge>}
|
||
</div>
|
||
<div className="text-sm text-gray-400 game-mono">
|
||
⚔️ {fmt(calcDamage(store, store.activeSpell))} dmg •
|
||
<span style={{ color: getSpellCostColor(activeSpellDef.cost) }}>
|
||
{' '}{formatSpellCost(activeSpellDef.cost)}
|
||
</span>
|
||
{' '}• ⚡ {(activeSpellDef.castSpeed || 1).toFixed(1)} casts/hr
|
||
</div>
|
||
|
||
{/* Cast progress bar when climbing */}
|
||
{store.currentAction === 'climb' && (
|
||
<div className="space-y-1">
|
||
<div className="flex justify-between text-xs text-gray-400">
|
||
<span>Cast Progress</span>
|
||
<span>{((store.castProgress || 0) * 100).toFixed(0)}%</span>
|
||
</div>
|
||
<Progress value={Math.min(100, (store.castProgress || 0) * 100)} className="h-2 bg-gray-800" />
|
||
</div>
|
||
)}
|
||
|
||
{activeSpellDef.desc && (
|
||
<div className="text-xs text-gray-500 italic">{activeSpellDef.desc}</div>
|
||
)}
|
||
{activeSpellDef.effects && activeSpellDef.effects.length > 0 && (
|
||
<div className="flex gap-1 flex-wrap">
|
||
{activeSpellDef.effects.map((eff, i) => (
|
||
<Badge key={i} variant="outline" className="text-xs">
|
||
{eff.type === 'lifesteal' && `🩸 ${Math.round(eff.value * 100)}% lifesteal`}
|
||
{eff.type === 'burn' && `🔥 Burn`}
|
||
{eff.type === 'freeze' && `❄️ Freeze`}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
) : (
|
||
<div className="text-gray-500">No spell selected</div>
|
||
)}
|
||
|
||
{/* Can cast indicator */}
|
||
{activeSpellDef && (
|
||
<div className={`text-xs ${canCastSpell(store.activeSpell) ? 'text-green-400' : 'text-red-400'}`}>
|
||
{canCastSpell(store.activeSpell) ? '✓ Can cast' : '✗ Insufficient mana'}
|
||
</div>
|
||
)}
|
||
|
||
{incursionStrength > 0 && (
|
||
<div className="p-2 bg-red-900/20 border border-red-800/50 rounded">
|
||
<div className="text-xs text-red-400 game-panel-title mb-1">LABYRINTH INCURSION</div>
|
||
<div className="text-sm text-gray-300">
|
||
-{Math.round(incursionStrength * 100)}% mana regen
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Current Study (if any) */}
|
||
{store.currentStudyTarget && (
|
||
<Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2">
|
||
<CardContent className="pt-4 space-y-3">
|
||
{renderStudyProgress()}
|
||
|
||
{/* Parallel Study Progress */}
|
||
{store.parallelStudyTarget && (
|
||
<div className="p-3 rounded border border-cyan-600/50 bg-cyan-900/20">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
<BookOpen className="w-4 h-4 text-cyan-400" />
|
||
<span className="text-sm font-semibold text-cyan-300">
|
||
Parallel: {SKILLS_DEF[store.parallelStudyTarget.id]?.name}
|
||
{store.parallelStudyTarget.type === 'skill' && ` Lv.${(store.skills[store.parallelStudyTarget.id] || 0) + 1}`}
|
||
</span>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
||
onClick={() => store.cancelParallelStudy()}
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
<Progress value={Math.min(100, (store.parallelStudyTarget.progress / store.parallelStudyTarget.required) * 100)} className="h-2 bg-gray-800" />
|
||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
||
<span>{formatStudyTime(store.parallelStudyTarget.progress)} / {formatStudyTime(store.parallelStudyTarget.required)}</span>
|
||
<span>50% speed (Parallel Study)</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Crafting Progress (if any) */}
|
||
{(store.designProgress || store.preparationProgress || store.applicationProgress) && (
|
||
<Card className="bg-gray-900/80 border-cyan-600/50 lg:col-span-2">
|
||
<CardContent className="pt-4">
|
||
{renderCraftingProgress()}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Spells Available */}
|
||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Known Spells</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
|
||
{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 (
|
||
<Button
|
||
key={id}
|
||
variant="outline"
|
||
className={`h-auto py-2 px-3 flex flex-col items-start ${isActive ? 'border-amber-500 bg-amber-900/20' : canCast ? 'border-gray-600 bg-gray-800/50 hover:bg-gray-700/50' : 'border-gray-700 bg-gray-800/30 opacity-60'}`}
|
||
onClick={() => store.setSpell(id)}
|
||
>
|
||
<div className="text-sm font-semibold" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
|
||
{def.name}
|
||
</div>
|
||
<div className="text-xs text-gray-400 game-mono">
|
||
{fmt(calcDamage(store, id))} dmg
|
||
</div>
|
||
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
|
||
{formatSpellCost(def.cost)}
|
||
</div>
|
||
</Button>
|
||
);
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Activity Log */}
|
||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<ScrollArea className="h-32">
|
||
<div className="space-y-1">
|
||
{store.log.slice(0, 20).map((entry, i) => (
|
||
<div
|
||
key={i}
|
||
className={`text-sm ${i === 0 ? 'text-gray-200' : 'text-gray-500'} italic`}
|
||
>
|
||
{entry}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</ScrollArea>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
|
||
// 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 (
|
||
<div className="space-y-6">
|
||
{/* Equipment-Granted Spells */}
|
||
<div className="mb-6">
|
||
<h3 className="text-lg font-semibold mb-3 text-cyan-400">✨ Known Spells</h3>
|
||
<p className="text-sm text-gray-400 mb-4">
|
||
Spells are obtained by enchanting equipment with spell effects.
|
||
Visit the Crafting tab to design and apply enchantments.
|
||
</p>
|
||
|
||
{equipmentSpells.length > 0 ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{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 (
|
||
<Card
|
||
key={id}
|
||
className={`bg-gray-900/80 border-cyan-600/50 ${canCast ? 'ring-1 ring-green-500/30' : ''}`}
|
||
>
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-sm" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
|
||
{def.name}
|
||
</CardTitle>
|
||
<Badge className="bg-cyan-900/50 text-cyan-300 text-xs">Equipment</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
<div className="text-xs text-gray-400">
|
||
{def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>}
|
||
<span>⚔️ {def.dmg} dmg</span>
|
||
</div>
|
||
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
|
||
Cost: {formatSpellCost(def.cost)}
|
||
</div>
|
||
<div className="text-xs text-cyan-400/70">From: {source}</div>
|
||
<div className="flex gap-2">
|
||
{isActive ? (
|
||
<Badge className="bg-amber-900/50 text-amber-300">Active</Badge>
|
||
) : (
|
||
<Button size="sm" variant="outline" onClick={() => store.setSpell(id)}>
|
||
Set Active
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<div className="text-center p-8 bg-gray-800/30 rounded border border-gray-700">
|
||
<div className="text-gray-500 mb-2">No spells known yet</div>
|
||
<div className="text-sm text-gray-600">Enchant a staff with a spell effect to gain spells</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Pact Spells (from guardian defeats) */}
|
||
{store.signedPacts.length > 0 && (
|
||
<div className="mb-6">
|
||
<h3 className="text-lg font-semibold mb-3 text-amber-400">🏆 Pact Spells</h3>
|
||
<p className="text-sm text-gray-400 mb-3">Spells earned through guardian pacts appear here.</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Spell Reference - show all available spells for enchanting */}
|
||
<div className="mb-6">
|
||
<h3 className="text-lg font-semibold mb-3 text-purple-400">📚 Spell Reference</h3>
|
||
<p className="text-sm text-gray-400 mb-4">
|
||
These spells can be applied to equipment through the enchanting system.
|
||
Research enchantment effects in the Skills tab to unlock them for designing.
|
||
</p>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{Object.entries(SPELLS_DEF).map(([id, def]) => {
|
||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
||
const isUnlocked = store.unlockedEffects?.includes(`spell_${id}`);
|
||
|
||
return (
|
||
<Card
|
||
key={id}
|
||
className={`bg-gray-900/80 border-gray-700 ${isUnlocked ? 'border-purple-500/50' : 'opacity-60'}`}
|
||
>
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-sm" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
|
||
{def.name}
|
||
</CardTitle>
|
||
<div className="flex gap-1">
|
||
{def.tier > 0 && <Badge variant="outline" className="text-xs">T{def.tier}</Badge>}
|
||
{isUnlocked && <Badge className="bg-purple-900/50 text-purple-300 text-xs">Unlocked</Badge>}
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-2">
|
||
<div className="text-xs text-gray-400">
|
||
{def.elem !== 'raw' && <span className="mr-2">{elemDef?.sym} {elemDef?.name}</span>}
|
||
<span>⚔️ {def.dmg} dmg</span>
|
||
</div>
|
||
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
|
||
Cost: {formatSpellCost(def.cost)}
|
||
</div>
|
||
{def.desc && (
|
||
<div className="text-xs text-gray-500 italic">{def.desc}</div>
|
||
)}
|
||
{!isUnlocked && (
|
||
<div className="text-xs text-amber-400/70">Research to unlock for enchanting</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Lab Tab
|
||
const renderLabTab = () => (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
{/* Elemental Mana Display */}
|
||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Elemental Mana</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-2">
|
||
{Object.entries(store.elements)
|
||
.filter(([, state]) => state.unlocked)
|
||
.map(([id, state]) => {
|
||
const def = ELEMENTS[id];
|
||
const isSelected = convertTarget === id;
|
||
return (
|
||
<div
|
||
key={id}
|
||
className={`p-2 rounded border cursor-pointer transition-all ${isSelected ? 'border-blue-500 bg-blue-900/20' : 'border-gray-700 bg-gray-800/50 hover:border-gray-600'}`}
|
||
style={{ borderColor: isSelected ? def?.color : undefined }}
|
||
onClick={() => setConvertTarget(id)}
|
||
>
|
||
<div className="text-lg text-center">{def?.sym}</div>
|
||
<div className="text-xs font-semibold text-center" style={{ color: def?.color }}>{def?.name}</div>
|
||
<div className="text-xs text-gray-400 game-mono text-center">{state.current}/{state.max}</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Element Conversion */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Element Conversion</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-sm text-gray-400 mb-3">
|
||
Convert raw mana to elemental mana (100:1 ratio)
|
||
</p>
|
||
|
||
<div className="flex gap-2 flex-wrap">
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => store.convertMana(convertTarget, 1)}
|
||
disabled={!store.elements[convertTarget]?.unlocked || store.rawMana < MANA_PER_ELEMENT}
|
||
>
|
||
+1 ({MANA_PER_ELEMENT})
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => store.convertMana(convertTarget, 10)}
|
||
disabled={!store.elements[convertTarget]?.unlocked || store.rawMana < MANA_PER_ELEMENT * 10}
|
||
>
|
||
+10 ({MANA_PER_ELEMENT * 10})
|
||
</Button>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
onClick={() => store.convertMana(convertTarget, 100)}
|
||
disabled={!store.elements[convertTarget]?.unlocked || store.rawMana < MANA_PER_ELEMENT * 100}
|
||
>
|
||
+100 ({MANA_PER_ELEMENT * 100})
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Unlock Elements */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Unlock Elements</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-sm text-gray-400 mb-3">
|
||
Unlock new elemental affinities (500 mana each)
|
||
</p>
|
||
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||
{Object.entries(store.elements)
|
||
.filter(([id, state]) => !state.unlocked && ELEMENTS[id]?.cat !== 'exotic')
|
||
.map(([id]) => {
|
||
const def = ELEMENTS[id];
|
||
return (
|
||
<div
|
||
key={id}
|
||
className="p-2 rounded border border-gray-700 bg-gray-800/50"
|
||
>
|
||
<div className="text-lg opacity-50">{def?.sym}</div>
|
||
<div className="text-xs font-semibold text-gray-500">{def?.name}</div>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
className="mt-1 w-full"
|
||
disabled={store.rawMana < 500}
|
||
onClick={() => store.unlockElement(id)}
|
||
>
|
||
Unlock
|
||
</Button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Composite Crafting */}
|
||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Composite & Exotic Crafting</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||
{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 (
|
||
<div
|
||
key={id}
|
||
className={`p-3 rounded border ${canCraft ? 'border-gray-600 bg-gray-800/50' : 'border-gray-700 bg-gray-800/30 opacity-50'}`}
|
||
>
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className="text-2xl">{def.sym}</span>
|
||
<div>
|
||
<div className="text-sm font-semibold" style={{ color: def.color }}>
|
||
{def.name}
|
||
</div>
|
||
<div className="text-xs text-gray-500">{def.cat}</div>
|
||
</div>
|
||
</div>
|
||
<div className="text-xs text-gray-400 mb-2">
|
||
{recipe.map((r) => ELEMENTS[r]?.sym).join(' + ')}
|
||
</div>
|
||
<Button
|
||
size="sm"
|
||
variant={canCraft ? 'default' : 'outline'}
|
||
className="w-full"
|
||
disabled={!canCraft}
|
||
onClick={() => store.craftComposite(id)}
|
||
>
|
||
Craft
|
||
</Button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
|
||
// 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 (
|
||
<Dialog open={!!upgradeDialogSkill} onOpenChange={(open) => {
|
||
if (!open) {
|
||
setPendingUpgradeSelections([]);
|
||
setUpgradeDialogSkill(null);
|
||
}
|
||
}}>
|
||
<DialogContent className="bg-gray-900 border-gray-700 max-w-lg">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-amber-400">
|
||
Choose Upgrade - {skillDef?.name || upgradeDialogSkill}
|
||
</DialogTitle>
|
||
<DialogDescription className="text-gray-400">
|
||
Level {upgradeDialogMilestone} Milestone - Select 2 upgrades ({currentSelections.length}/2 chosen)
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<div className="space-y-2 mt-4">
|
||
{available.map((upgrade) => {
|
||
const isSelected = currentSelections.includes(upgrade.id);
|
||
const canToggle = currentSelections.length < 2 || isSelected;
|
||
|
||
return (
|
||
<div
|
||
key={upgrade.id}
|
||
className={`p-3 rounded border cursor-pointer transition-all ${
|
||
isSelected
|
||
? 'border-amber-500 bg-amber-900/30'
|
||
: canToggle
|
||
? 'border-gray-600 bg-gray-800/50 hover:border-amber-500/50 hover:bg-gray-800'
|
||
: 'border-gray-700 bg-gray-800/30 opacity-50 cursor-not-allowed'
|
||
}`}
|
||
onClick={() => {
|
||
if (canToggle) {
|
||
toggleUpgrade(upgrade.id);
|
||
}
|
||
}}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<div className="font-semibold text-sm text-amber-300">{upgrade.name}</div>
|
||
{isSelected && <Badge className="bg-amber-600 text-amber-100">Selected</Badge>}
|
||
</div>
|
||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||
{upgrade.effect.type === 'multiplier' && (
|
||
<div className="text-xs text-green-400 mt-1">
|
||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||
</div>
|
||
)}
|
||
{upgrade.effect.type === 'bonus' && (
|
||
<div className="text-xs text-blue-400 mt-1">
|
||
+{upgrade.effect.value} {upgrade.effect.stat}
|
||
</div>
|
||
)}
|
||
{upgrade.effect.type === 'special' && (
|
||
<div className="text-xs text-cyan-400 mt-1">
|
||
⚡ {upgrade.effect.specialDesc || 'Special effect'}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<div className="flex justify-end gap-2 mt-4">
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleCancel}
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
variant="default"
|
||
onClick={handleDone}
|
||
disabled={currentSelections.length !== 2}
|
||
>
|
||
{currentSelections.length < 2 ? `Select ${2 - currentSelections.length} more` : 'Confirm'}
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|
||
|
||
// Skills Tab
|
||
const renderSkillsTab = () => (
|
||
<div className="space-y-4">
|
||
{/* Upgrade Selection Dialog */}
|
||
{renderUpgradeDialog()}
|
||
|
||
{/* Current Study Progress */}
|
||
{store.currentStudyTarget && store.currentStudyTarget.type === 'skill' && (
|
||
<Card className="bg-gray-900/80 border-purple-600/50">
|
||
<CardContent className="pt-4">
|
||
{renderStudyProgress()}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{SKILL_CATEGORIES.map((cat) => {
|
||
const skillsInCat = Object.entries(SKILLS_DEF).filter(([, def]) => def.cat === cat.id);
|
||
if (skillsInCat.length === 0) return null;
|
||
|
||
return (
|
||
<Card key={cat.id} className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
||
{cat.icon} {cat.name}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-2">
|
||
{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 (
|
||
<div
|
||
key={id}
|
||
className={`flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded border gap-2 ${
|
||
isStudying ? 'border-purple-500 bg-purple-900/20' :
|
||
milestoneInfo ? 'border-amber-500/50 bg-amber-900/10' :
|
||
'border-gray-700 bg-gray-800/30'
|
||
}`
|
||
}
|
||
>
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<span className="font-semibold text-sm">{skillDisplayName}</span>
|
||
{/* Show tier */}
|
||
{currentTier > 1 && (
|
||
<Badge className="bg-purple-600/50 text-purple-200 text-xs">Tier {currentTier} ({fmtDec(tierMultiplier, 0)}x)</Badge>
|
||
)}
|
||
{level > 0 && <span className="text-purple-400 text-sm">Lv.{level}</span>}
|
||
{/* Show selected upgrades */}
|
||
{selectedUpgrades.length > 0 && (
|
||
<div className="flex gap-1">
|
||
{selectedL5.length > 0 && (
|
||
<Badge className="bg-amber-700/50 text-amber-200 text-xs">L5: {selectedL5.length}</Badge>
|
||
)}
|
||
{selectedL10.length > 0 && (
|
||
<Badge className="bg-purple-700/50 text-purple-200 text-xs">L10: {selectedL10.length}</Badge>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="text-xs text-gray-400 italic">{def.desc}{currentTier > 1 && ` (Tier ${currentTier}: ${fmtDec(tierMultiplier, 0)}x effect)`}</div>
|
||
{!prereqMet && def.req && (
|
||
<div className="text-xs text-red-400 mt-1">
|
||
Requires: {Object.entries(def.req).map(([r, rl]) => `${SKILLS_DEF[r]?.name} Lv.${rl}`).join(', ')}
|
||
</div>
|
||
)}
|
||
<div className="text-xs text-gray-500 mt-1">
|
||
<span className={effectiveSpeedMult > 1 ? 'text-green-400' : ''}>
|
||
Study: {formatStudyTime(effectiveStudyTime)}{effectiveSpeedMult > 1 && <span className="text-xs ml-1">({Math.round(effectiveSpeedMult * 100)}% speed)</span>}
|
||
</span>
|
||
{' • '}
|
||
<span className={costMult < 1 ? 'text-green-400' : ''}>
|
||
Cost: {fmt(cost)} mana{costMult < 1 && <span className="text-xs ml-1">({Math.round(costMult * 100)}% cost)</span>}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Milestone indicator */}
|
||
{milestoneInfo && (
|
||
<div className="text-xs text-amber-400 mt-1 flex items-center gap-1">
|
||
⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 flex-wrap sm:flex-nowrap">
|
||
{/* Level dots */}
|
||
<div className="flex gap-1 shrink-0">
|
||
{Array.from({ length: def.max }).map((_, i) => (
|
||
<div
|
||
key={i}
|
||
className={`w-2 h-2 rounded-full border ${
|
||
i < level ? 'bg-purple-500 border-purple-400' :
|
||
i === 4 || i === 9 ? 'border-amber-500' :
|
||
'border-gray-600'
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{isStudying ? (
|
||
<div className="text-xs text-purple-400">
|
||
{formatStudyTime(store.currentStudyTarget?.progress || 0)}/{formatStudyTime(tierStudyTime)}
|
||
</div>
|
||
) : milestoneInfo ? (
|
||
<Button
|
||
size="sm"
|
||
className="bg-amber-600 hover:bg-amber-700"
|
||
onClick={() => {
|
||
setUpgradeDialogSkill(tieredSkillId);
|
||
setUpgradeDialogMilestone(milestoneInfo.milestone);
|
||
}}
|
||
>
|
||
Choose Upgrades
|
||
</Button>
|
||
) : canTierUp ? (
|
||
<Button
|
||
size="sm"
|
||
className="bg-purple-600 hover:bg-purple-700"
|
||
onClick={() => store.tierUpSkill(tieredSkillId)}
|
||
>
|
||
⬆️ Tier Up
|
||
</Button>
|
||
) : maxed ? (
|
||
<Badge className="bg-green-900/50 text-green-300">Maxed</Badge>
|
||
) : (
|
||
<div className="flex gap-1">
|
||
<Button
|
||
size="sm"
|
||
variant={canStudy ? 'default' : 'outline'}
|
||
disabled={!canStudy}
|
||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||
onClick={() => store.startStudyingSkill(tieredSkillId)}
|
||
>
|
||
Study ({fmt(cost)})
|
||
</Button>
|
||
{/* Parallel Study button */}
|
||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
|
||
store.currentStudyTarget &&
|
||
!store.parallelStudyTarget &&
|
||
store.currentStudyTarget.id !== tieredSkillId &&
|
||
canStudy && (
|
||
<TooltipProvider>
|
||
<Tooltip>
|
||
<TooltipTrigger asChild>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
||
onClick={() => store.startParallelStudySkill(tieredSkillId)}
|
||
>
|
||
⚡
|
||
</Button>
|
||
</TooltipTrigger>
|
||
<TooltipContent>
|
||
<p>Study in parallel (50% speed)</p>
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</TooltipProvider>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
|
||
// 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 (
|
||
<div className="space-y-4">
|
||
{/* Mana Stats */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-blue-400 game-panel-title text-xs flex items-center gap-2">
|
||
<Droplet className="w-4 h-4" />
|
||
Mana Stats
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Base Max Mana:</span>
|
||
<span className="text-gray-200">100</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Mana Well Bonus:</span>
|
||
<span className="text-blue-300">
|
||
{(() => {
|
||
const mw = store.skillTiers?.manaWell || 1;
|
||
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
|
||
const level = store.skills[tieredSkillId] || store.skills.manaWell || 0;
|
||
const tierMult = getTierMultiplier(tieredSkillId);
|
||
return `+${fmt(level * 100 * tierMult)} (${level} lvl × 100 × ${tierMult}x tier)`;
|
||
})()}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Deep Reservoir Bonus:</span>
|
||
<span className="text-blue-300">+{fmt((store.skills.deepReservoir || 0) * 500)}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Prestige Mana Well:</span>
|
||
<span className="text-blue-300">+{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}</span>
|
||
</div>
|
||
{/* Skill Upgrade Effects on Max Mana */}
|
||
{upgradeEffects.maxManaBonus > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-amber-400">Upgrade Mana Bonus:</span>
|
||
<span className="text-amber-300">+{fmt(upgradeEffects.maxManaBonus)}</span>
|
||
</div>
|
||
)}
|
||
{upgradeEffects.maxManaMultiplier > 1 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-amber-400">Upgrade Mana Multiplier:</span>
|
||
<span className="text-amber-300">×{fmtDec(upgradeEffects.maxManaMultiplier, 2)}</span>
|
||
</div>
|
||
)}
|
||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||
<span className="text-gray-300">Total Max Mana:</span>
|
||
<span className="text-blue-400">{fmt(maxMana)}</span>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Base Regen:</span>
|
||
<span className="text-gray-200">2/hr</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Mana Flow Bonus:</span>
|
||
<span className="text-blue-300">
|
||
{(() => {
|
||
const mf = store.skillTiers?.manaFlow || 1;
|
||
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
|
||
const level = store.skills[tieredSkillId] || store.skills.manaFlow || 0;
|
||
const tierMult = getTierMultiplier(tieredSkillId);
|
||
return `+${fmtDec(level * 1 * tierMult)}/hr (${level} lvl × 1 × ${tierMult}x tier)`;
|
||
})()}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Mana Spring Bonus:</span>
|
||
<span className="text-blue-300">+{(store.skills.manaSpring || 0) * 2}/hr</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Prestige Mana Flow:</span>
|
||
<span className="text-blue-300">+{fmtDec((store.prestigeUpgrades.manaFlow || 0) * 0.5)}/hr</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Temporal Echo:</span>
|
||
<span className="text-blue-300">×{fmtDec(1 + (store.prestigeUpgrades.temporalEcho || 0) * 0.1, 2)}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||
<span className="text-gray-300">Base Regen:</span>
|
||
<span className="text-blue-400">{fmtDec(baseRegen, 2)}/hr</span>
|
||
</div>
|
||
{/* Skill Upgrade Effects on Regen */}
|
||
{upgradeEffects.regenBonus > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-amber-400">Upgrade Regen Bonus:</span>
|
||
<span className="text-amber-300">+{fmtDec(upgradeEffects.regenBonus, 2)}/hr</span>
|
||
</div>
|
||
)}
|
||
{upgradeEffects.permanentRegenBonus > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-amber-400">Permanent Regen Bonus:</span>
|
||
<span className="text-amber-300">+{fmtDec(upgradeEffects.permanentRegenBonus, 2)}/hr</span>
|
||
</div>
|
||
)}
|
||
{upgradeEffects.regenMultiplier > 1 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-amber-400">Upgrade Regen Multiplier:</span>
|
||
<span className="text-amber-300">×{fmtDec(upgradeEffects.regenMultiplier, 2)}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<Separator className="bg-gray-700 my-3" />
|
||
{/* Skill Upgrade Effects Summary */}
|
||
{upgradeEffects.activeUpgrades.length > 0 && (
|
||
<>
|
||
<div className="mb-2">
|
||
<span className="text-xs text-amber-400 game-panel-title">Active Skill Upgrades</span>
|
||
</div>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-3">
|
||
{upgradeEffects.activeUpgrades.map((upgrade, idx) => (
|
||
<div key={idx} className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
|
||
<span className="text-gray-300">{upgrade.name}</span>
|
||
<span className="text-gray-400">{upgrade.desc}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<Separator className="bg-gray-700 my-3" />
|
||
</>
|
||
)}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Click Mana Value:</span>
|
||
<span className="text-purple-300">+{clickMana}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Mana Tap Bonus:</span>
|
||
<span className="text-purple-300">+{store.skills.manaTap || 0}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Mana Surge Bonus:</span>
|
||
<span className="text-purple-300">+{(store.skills.manaSurge || 0) * 3}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Mana Overflow:</span>
|
||
<span className="text-purple-300">×{fmtDec(1 + (store.skills.manaOverflow || 0) * 0.25, 2)}</span>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Meditation Multiplier:</span>
|
||
<span className={`font-semibold ${meditationMultiplier > 1.5 ? 'text-purple-400' : 'text-gray-300'}`}>
|
||
{fmtDec(meditationMultiplier, 2)}x
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Effective Regen:</span>
|
||
<span className="text-green-400 font-semibold">{fmtDec(effectiveRegen, 2)}/hr</span>
|
||
</div>
|
||
{incursionStrength > 0 && !hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM) && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-red-400">Incursion Penalty:</span>
|
||
<span className="text-red-400">-{Math.round(incursionStrength * 100)}%</span>
|
||
</div>
|
||
)}
|
||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.STEADY_STREAM) && incursionStrength > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-green-400">Steady Stream:</span>
|
||
<span className="text-green-400">Immune to incursion</span>
|
||
</div>
|
||
)}
|
||
{manaCascadeBonus > 0 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-cyan-400">Mana Cascade Bonus:</span>
|
||
<span className="text-cyan-400">+{fmtDec(manaCascadeBonus, 2)}/hr</span>
|
||
</div>
|
||
)}
|
||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_TORRENT) && store.rawMana > maxMana * 0.75 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-cyan-400">Mana Torrent:</span>
|
||
<span className="text-cyan-400">+50% regen (high mana)</span>
|
||
</div>
|
||
)}
|
||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.DESPERATE_WELLS) && store.rawMana < maxMana * 0.25 && (
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-cyan-400">Desperate Wells:</span>
|
||
<span className="text-cyan-400">+50% regen (low mana)</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Combat Stats */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-red-400 game-panel-title text-xs flex items-center gap-2">
|
||
<Swords className="w-4 h-4" />
|
||
Combat Stats
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Active Spell Base Damage:</span>
|
||
<span className="text-gray-200">{activeSpellDef?.dmg || 5}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Combat Training Bonus:</span>
|
||
<span className="text-red-300">+{(store.skills.combatTrain || 0) * 5}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Arcane Fury Multiplier:</span>
|
||
<span className="text-red-300">×{fmtDec(1 + (store.skills.arcaneFury || 0) * 0.1, 2)}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Elemental Mastery:</span>
|
||
<span className="text-red-300">×{fmtDec(1 + (store.skills.elementalMastery || 0) * 0.15, 2)}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Guardian Bane:</span>
|
||
<span className="text-red-300">×{fmtDec(1 + (store.skills.guardianBane || 0) * 0.2, 2)} (vs guardians)</span>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Critical Hit Chance:</span>
|
||
<span className="text-amber-300">{((store.skills.precision || 0) * 5)}%</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Critical Multiplier:</span>
|
||
<span className="text-amber-300">1.5x</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Spell Echo Chance:</span>
|
||
<span className="text-amber-300">{((store.skills.spellEcho || 0) * 10)}%</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Pact Multiplier:</span>
|
||
<span className="text-amber-300">×{fmtDec(store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
|
||
<span className="text-gray-300">Total Damage:</span>
|
||
<span className="text-red-400">{fmt(calcDamage(store, store.activeSpell))}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Study Stats */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||
<BookOpen className="w-4 h-4" />
|
||
Study Stats
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Study Speed:</span>
|
||
<span className="text-purple-300">×{fmtDec(studySpeedMult, 2)}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Quick Learner Bonus:</span>
|
||
<span className="text-purple-300">+{((store.skills.quickLearner || 0) * 10)}%</span>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Study Cost:</span>
|
||
<span className="text-purple-300">{Math.round(studyCostMult * 100)}%</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Focused Mind Bonus:</span>
|
||
<span className="text-purple-300">-{((store.skills.focusedMind || 0) * 5)}%</span>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Progress Retention:</span>
|
||
<span className="text-purple-300">{Math.round((1 + (store.skills.knowledgeRetention || 0) * 0.2) * 100)}%</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Element Stats */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-green-400 game-panel-title text-xs flex items-center gap-2">
|
||
<FlaskConical className="w-4 h-4" />
|
||
Element Stats
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Element Capacity:</span>
|
||
<span className="text-green-300">{elemMax}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Elem. Attunement Bonus:</span>
|
||
<span className="text-green-300">
|
||
{(() => {
|
||
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}`;
|
||
})()}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Prestige Attunement:</span>
|
||
<span className="text-green-300">+{(store.prestigeUpgrades.elementalAttune || 0) * 25}</span>
|
||
</div>
|
||
</div>
|
||
<div className="space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Unlocked Elements:</span>
|
||
<span className="text-green-300">{Object.values(store.elements).filter(e => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Elem. Crafting Bonus:</span>
|
||
<span className="text-green-300">×{fmtDec(1 + (store.skills.elemCrafting || 0) * 0.25, 2)}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Separator className="bg-gray-700 my-3" />
|
||
<div className="text-xs text-gray-400 mb-2">Elemental Mana Pools:</div>
|
||
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
||
{Object.entries(store.elements)
|
||
.filter(([, state]) => state.unlocked)
|
||
.map(([id, state]) => {
|
||
const def = ELEMENTS[id];
|
||
return (
|
||
<div key={id} className="p-2 rounded border border-gray-700 bg-gray-800/50 text-center">
|
||
<div className="text-lg">{def?.sym}</div>
|
||
<div className="text-xs text-gray-400">{state.current}/{state.max}</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Active Upgrades */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||
<Star className="w-4 h-4" />
|
||
Active Skill Upgrades ({selectedUpgrades.length})
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{selectedUpgrades.length === 0 ? (
|
||
<div className="text-gray-500 text-sm">No skill upgrades selected yet. Level skills to 5 or 10 to choose upgrades.</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||
{selectedUpgrades.map(({ skillId, upgrade }) => (
|
||
<div key={upgrade.id} className="p-2 rounded border border-amber-600/30 bg-amber-900/10">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-amber-300 text-sm font-semibold">{upgrade.name}</span>
|
||
<Badge variant="outline" className="text-xs text-gray-400">
|
||
{SKILLS_DEF[skillId]?.name || skillId}
|
||
</Badge>
|
||
</div>
|
||
<div className="text-xs text-gray-400 mt-1">{upgrade.desc}</div>
|
||
{upgrade.effect.type === 'multiplier' && (
|
||
<div className="text-xs text-green-400 mt-1">
|
||
+{Math.round((upgrade.effect.value! - 1) * 100)}% {upgrade.effect.stat}
|
||
</div>
|
||
)}
|
||
{upgrade.effect.type === 'bonus' && (
|
||
<div className="text-xs text-blue-400 mt-1">
|
||
+{upgrade.effect.value} {upgrade.effect.stat}
|
||
</div>
|
||
)}
|
||
{upgrade.effect.type === 'special' && (
|
||
<div className="text-xs text-cyan-400 mt-1">
|
||
⚡ {upgrade.effect.specialDesc || 'Special effect active'}
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Pact Bonuses */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||
<Trophy className="w-4 h-4" />
|
||
Signed Pacts ({store.signedPacts.length}/10)
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{store.signedPacts.length === 0 ? (
|
||
<div className="text-gray-500 text-sm">No pacts signed yet. Defeat guardians to earn pacts.</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{store.signedPacts.map((floor) => {
|
||
const guardian = GUARDIANS[floor];
|
||
if (!guardian) return null;
|
||
return (
|
||
<div
|
||
key={floor}
|
||
className="flex items-center justify-between p-2 rounded border"
|
||
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
|
||
>
|
||
<div>
|
||
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
||
{guardian.name}
|
||
</div>
|
||
<div className="text-xs text-gray-400">Floor {floor}</div>
|
||
</div>
|
||
<Badge className="bg-amber-900/50 text-amber-300">
|
||
{guardian.pact}x multiplier
|
||
</Badge>
|
||
</div>
|
||
);
|
||
})}
|
||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2 mt-2">
|
||
<span className="text-gray-300">Combined Pact Multiplier:</span>
|
||
<span className="text-amber-400">×{fmtDec(store.signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Loop Stats */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||
<RotateCcw className="w-4 h-4" />
|
||
Loop Stats
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
|
||
<div className="text-xs text-gray-400">Loops Completed</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
|
||
<div className="text-xs text-gray-400">Current Insight</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
|
||
<div className="text-xs text-gray-400">Total Insight</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||
<div className="text-2xl font-bold text-green-400 game-mono">{store.maxFloorReached}</div>
|
||
<div className="text-xs text-gray-400">Max Floor</div>
|
||
</div>
|
||
</div>
|
||
<Separator className="bg-gray-700 my-3" />
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(store.spells).filter(s => s.learned).length}</div>
|
||
<div className="text-xs text-gray-400">Spells Learned</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(store.skills).reduce((a, b) => a + b, 0)}</div>
|
||
<div className="text-xs text-gray-400">Total Skill Levels</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||
<div className="text-xl font-bold text-gray-300 game-mono">{fmt(store.totalManaGathered)}</div>
|
||
<div className="text-xs text-gray-400">Total Mana Gathered</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||
<div className="text-xl font-bold text-gray-300 game-mono">{store.memorySlots}</div>
|
||
<div className="text-xs text-gray-400">Memory Slots</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// Grimoire Tab (Prestige)
|
||
const renderGrimoireTab = () => (
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||
{/* Current Status */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Loop Status</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="p-3 bg-gray-800/50 rounded">
|
||
<div className="text-2xl font-bold text-amber-400 game-mono">{store.loopCount}</div>
|
||
<div className="text-xs text-gray-400">Loops Completed</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded">
|
||
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(store.insight)}</div>
|
||
<div className="text-xs text-gray-400">Current Insight</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded">
|
||
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(store.totalInsight)}</div>
|
||
<div className="text-xs text-gray-400">Total Insight</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800/50 rounded">
|
||
<div className="text-2xl font-bold text-green-400 game-mono">{store.memorySlots}</div>
|
||
<div className="text-xs text-gray-400">Memory Slots</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Signed Pacts */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Signed Pacts</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{store.signedPacts.length === 0 ? (
|
||
<div className="text-gray-500 text-sm">No pacts signed yet. Defeat guardians to earn pacts.</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{store.signedPacts.map((floor) => {
|
||
const guardian = GUARDIANS[floor];
|
||
if (!guardian) return null;
|
||
return (
|
||
<div
|
||
key={floor}
|
||
className="flex items-center justify-between p-2 rounded border"
|
||
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
|
||
>
|
||
<div>
|
||
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
||
{guardian.name}
|
||
</div>
|
||
<div className="text-xs text-gray-400">Floor {floor}</div>
|
||
</div>
|
||
<Badge className="bg-amber-900/50 text-amber-300">
|
||
{guardian.pact}x multiplier
|
||
</Badge>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Prestige Upgrades */}
|
||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-amber-400 game-panel-title text-xs">Insight Upgrades (Permanent)</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||
{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 (
|
||
<div
|
||
key={id}
|
||
className="p-3 rounded border border-gray-700 bg-gray-800/50"
|
||
>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="font-semibold text-amber-400 text-sm">{def.name}</div>
|
||
<Badge variant="outline" className="text-xs">
|
||
{level}/{def.max}
|
||
</Badge>
|
||
</div>
|
||
<div className="text-xs text-gray-400 italic mb-2">{def.desc}</div>
|
||
<Button
|
||
size="sm"
|
||
variant={canBuy ? 'default' : 'outline'}
|
||
className="w-full"
|
||
disabled={!canBuy}
|
||
onClick={() => store.doPrestige(id)}
|
||
>
|
||
{maxed ? 'Maxed' : `Upgrade (${fmt(def.cost)} insight)`}
|
||
</Button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Reset Game Button */}
|
||
<div className="mt-4 pt-4 border-t border-gray-700">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<div className="text-sm text-gray-400">Reset All Progress</div>
|
||
<div className="text-xs text-gray-500">Clear all data and start fresh</div>
|
||
</div>
|
||
<Button
|
||
size="sm"
|
||
variant="outline"
|
||
className="border-red-600/50 text-red-400 hover:bg-red-900/20"
|
||
onClick={() => {
|
||
if (confirm('Are you sure you want to reset ALL progress? This cannot be undone!')) {
|
||
store.resetGame();
|
||
}
|
||
}}
|
||
>
|
||
<RotateCcw className="w-4 h-4 mr-1" />
|
||
Reset
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
|
||
// Game Over Screen
|
||
if (store.gameOver) {
|
||
return (
|
||
<div className="fixed inset-0 game-overlay flex items-center justify-center z-50">
|
||
<Card className="bg-gray-900 border-gray-600 max-w-md w-full mx-4 shadow-2xl">
|
||
<CardHeader>
|
||
<CardTitle className={`text-3xl text-center game-title ${store.victory ? 'text-amber-400' : 'text-red-400'}`}>
|
||
{store.victory ? '🏆 VICTORY!' : '⏰ LOOP ENDS'}
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<p className="text-center text-gray-400">
|
||
{store.victory
|
||
? 'The Awakened One falls! Your power echoes through eternity.'
|
||
: 'The time loop resets... but you remember.'}
|
||
</p>
|
||
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<div className="p-3 bg-gray-800 rounded">
|
||
<div className="text-xl font-bold text-amber-400 game-mono">{fmt(store.loopInsight)}</div>
|
||
<div className="text-xs text-gray-400">Insight Gained</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800 rounded">
|
||
<div className="text-xl font-bold text-blue-400 game-mono">{store.maxFloorReached}</div>
|
||
<div className="text-xs text-gray-400">Best Floor</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800 rounded">
|
||
<div className="text-xl font-bold text-purple-400 game-mono">{store.signedPacts.length}</div>
|
||
<div className="text-xs text-gray-400">Pacts Signed</div>
|
||
</div>
|
||
<div className="p-3 bg-gray-800 rounded">
|
||
<div className="text-xl font-bold text-green-400 game-mono">{store.loopCount + 1}</div>
|
||
<div className="text-xs text-gray-400">Total Loops</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Button
|
||
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700"
|
||
size="lg"
|
||
onClick={() => store.startNewLoop()}
|
||
>
|
||
Begin New Loop
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<TooltipProvider>
|
||
<div className="game-root min-h-screen flex flex-col">
|
||
{/* Header */}
|
||
<header className="sticky top-0 z-50 bg-gradient-to-b from-gray-900 to-gray-900/80 border-b border-gray-700 px-4 py-2">
|
||
<div className="flex items-center justify-between">
|
||
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
|
||
|
||
<div className="flex items-center gap-4">
|
||
<div className="text-center">
|
||
<div className="text-lg font-bold game-mono text-amber-400">
|
||
Day {store.day}
|
||
</div>
|
||
<div className="text-xs text-gray-400">
|
||
{formatTime(store.hour)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="text-center">
|
||
<div className="text-lg font-bold game-mono text-purple-400">
|
||
{fmt(store.insight)}
|
||
</div>
|
||
<div className="text-xs text-gray-400">Insight</div>
|
||
</div>
|
||
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => store.togglePause()}
|
||
className="text-gray-400 hover:text-white"
|
||
>
|
||
{store.paused ? <Play className="w-4 h-4" /> : <Pause className="w-4 h-4" />}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Calendar */}
|
||
<div className="mt-2 flex gap-1 overflow-x-auto pb-1">
|
||
{renderCalendar()}
|
||
</div>
|
||
</header>
|
||
|
||
{/* Main Content */}
|
||
<div className="flex-1 flex flex-col md:flex-row">
|
||
{/* Sidebar */}
|
||
<aside className="w-full md:w-64 bg-gray-900/50 border-b md:border-b-0 md:border-r border-gray-700 p-4 space-y-4">
|
||
{/* Mana Panel */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardContent className="pt-4 space-y-3">
|
||
<div>
|
||
<div className="flex items-baseline gap-1">
|
||
<span className="text-3xl font-bold game-mono text-blue-400">{fmt(store.rawMana)}</span>
|
||
<span className="text-sm text-gray-400">/ {fmt(maxMana)}</span>
|
||
</div>
|
||
<div className="text-xs text-gray-400">
|
||
+{fmtDec(effectiveRegen)} mana/hr {meditationMultiplier > 1.01 && <span className="text-purple-400">({fmtDec(meditationMultiplier, 1)}x med)</span>}
|
||
</div>
|
||
</div>
|
||
|
||
<Progress
|
||
value={(store.rawMana / maxMana) * 100}
|
||
className="h-2 bg-gray-800"
|
||
/>
|
||
|
||
<Button
|
||
className={`w-full bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 ${isGathering ? 'animate-pulse' : ''}`}
|
||
onMouseDown={handleGatherStart}
|
||
onMouseUp={handleGatherEnd}
|
||
onMouseLeave={handleGatherEnd}
|
||
onTouchStart={handleGatherStart}
|
||
onTouchEnd={handleGatherEnd}
|
||
>
|
||
<Zap className="w-4 h-4 mr-2" />
|
||
Gather +{clickMana} Mana
|
||
{isGathering && <span className="ml-2 text-xs">(Holding...)</span>}
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Actions */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardContent className="pt-4">
|
||
<div className="text-xs text-amber-400 game-panel-title mb-2">Current Action</div>
|
||
{renderActionButtons()}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Floor Status */}
|
||
<Card className="bg-gray-900/80 border-gray-700">
|
||
<CardContent className="pt-4 space-y-2">
|
||
<div className="text-xs text-amber-400 game-panel-title mb-2">Floor Status</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm text-gray-400">Current</span>
|
||
<span className="text-lg font-bold" style={{ color: floorElemDef?.color }}>
|
||
{store.currentFloor}
|
||
</span>
|
||
</div>
|
||
<Progress
|
||
value={(store.floorHP / store.floorMaxHP) * 100}
|
||
className="h-2 bg-gray-800"
|
||
/>
|
||
<div className="text-xs text-gray-400 game-mono">
|
||
{fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</aside>
|
||
|
||
{/* Main Tab Content */}
|
||
<main className="flex-1 p-4 overflow-auto">
|
||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||
<TabsList className="mb-4 bg-gray-800/50 w-full overflow-x-auto flex-nowrap justify-start sm:justify-center">
|
||
<TabsTrigger value="spire" className="data-[state=active]:bg-gray-700 shrink-0">⚔️ Spire</TabsTrigger>
|
||
<TabsTrigger value="spells" className="data-[state=active]:bg-gray-700 shrink-0">📖 Spells</TabsTrigger>
|
||
<TabsTrigger value="lab" className="data-[state=active]:bg-gray-700 shrink-0">🧪 Lab</TabsTrigger>
|
||
<TabsTrigger value="crafting" className="data-[state=active]:bg-gray-700 shrink-0">✨ Crafting</TabsTrigger>
|
||
<TabsTrigger value="skills" className="data-[state=active]:bg-gray-700 shrink-0">📚 Skills</TabsTrigger>
|
||
<TabsTrigger value="grimoire" className="data-[state=active]:bg-gray-700 shrink-0">⭐ Grimoire</TabsTrigger>
|
||
<TabsTrigger value="stats" className="data-[state=active]:bg-gray-700 shrink-0">📊 Stats</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="spire">
|
||
{renderSpireTab()}
|
||
</TabsContent>
|
||
|
||
<TabsContent value="spells">
|
||
{renderSpellsTab()}
|
||
</TabsContent>
|
||
|
||
<TabsContent value="lab">
|
||
{renderLabTab()}
|
||
</TabsContent>
|
||
<TabsContent value="crafting">
|
||
<CraftingTab
|
||
equippedInstances={store.equippedInstances}
|
||
equipmentInstances={store.equipmentInstances}
|
||
enchantmentDesigns={store.enchantmentDesigns}
|
||
designProgress={store.designProgress}
|
||
preparationProgress={store.preparationProgress}
|
||
applicationProgress={store.applicationProgress}
|
||
rawMana={store.rawMana}
|
||
skills={store.skills}
|
||
currentAction={store.currentAction}
|
||
unlockedEffects={store.unlockedEffects || ['spell_manaBolt']}
|
||
startDesigningEnchantment={store.startDesigningEnchantment}
|
||
cancelDesign={store.cancelDesign}
|
||
saveDesign={store.saveDesign}
|
||
deleteDesign={store.deleteDesign}
|
||
startPreparing={store.startPreparing}
|
||
cancelPreparation={store.cancelPreparation}
|
||
startApplying={store.startApplying}
|
||
pauseApplication={store.pauseApplication}
|
||
resumeApplication={store.resumeApplication}
|
||
cancelApplication={store.cancelApplication}
|
||
disenchantEquipment={store.disenchantEquipment}
|
||
getAvailableCapacity={store.getAvailableCapacity}
|
||
/>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="skills">
|
||
{renderSkillsTab()}
|
||
</TabsContent>
|
||
|
||
<TabsContent value="grimoire">
|
||
{renderGrimoireTab()}
|
||
</TabsContent>
|
||
|
||
<TabsContent value="stats">
|
||
{renderStatsTab()}
|
||
</TabsContent>
|
||
</Tabs>
|
||
</main>
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<footer className="sticky bottom-0 bg-gray-900/80 border-t border-gray-700 px-4 py-2 text-center text-xs text-gray-500">
|
||
<span className="text-gray-400">Loop {store.loopCount + 1}</span>
|
||
{' • '}
|
||
<span>Pacts: {store.signedPacts.length}/10</span>
|
||
{' • '}
|
||
<span>Spells: {Object.values(store.spells).filter(s => s.learned).length}</span>
|
||
{' • '}
|
||
<span>Skills: {Object.values(store.skills).reduce((a, b) => a + b, 0)}</span>
|
||
</footer>
|
||
</div>
|
||
</TooltipProvider>
|
||
);
|
||
}
|