Phase 3: Extract sub-components from StatsTab.tsx

This commit is contained in:
Refactoring Agent
2026-04-24 14:45:07 +02:00
parent eea5ed1585
commit 7d1bfbe4dc
7 changed files with 480 additions and 355 deletions
@@ -0,0 +1,64 @@
'use client';
import { GUARDIANS } from '@/lib/game/constants';
import { fmtDec } from '@/lib/game/store';
import type { GameStore } from '@/lib/game/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Swords } from 'lucide-react';
export interface CombatStatsSectionProps {
store: GameStore;
}
export function CombatStatsSection({ store }: CombatStatsSectionProps) {
return (
<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">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>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,217 @@
'use client';
import { getTierMultiplier } from '@/lib/game/skill-evolution';
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
import { fmt, fmtDec } from '@/lib/game/store';
import type { GameStore, UnifiedEffects } from '@/lib/game/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Droplet } from 'lucide-react';
export interface ManaStatsSectionProps {
store: GameStore;
upgradeEffects: UnifiedEffects;
maxMana: number;
baseRegen: number;
clickMana: number;
meditationMultiplier: number;
effectiveRegen: number;
incursionStrength: number;
manaCascadeBonus: number;
}
export function ManaStatsSection({
store,
upgradeEffects,
maxMana,
baseRegen,
clickMana,
meditationMultiplier,
effectiveRegen,
incursionStrength,
manaCascadeBonus,
}: ManaStatsSectionProps) {
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-blue-400 game-panel-title text-xs flex items-center gap-2">
<Droplet className="w-4 h-4" />
Mana Stats
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Base Max Mana:</span>
<span className="text-gray-200">100</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Mana Well Bonus:</span>
<span className="text-blue-300">
{(() => {
const mw = 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">Prestige Mana Well:</span>
<span className="text-blue-300">+{fmt((store.prestigeUpgrades.manaWell || 0) * 500)}</span>
</div>
{upgradeEffects.maxManaBonus > 0 && (
<div className="flex justify-between text-sm">
<span className="text-amber-400">Upgrade Mana Bonus:</span>
<span className="text-amber-300">+{fmt(upgradeEffects.maxManaBonus)}</span>
</div>
)}
{upgradeEffects.maxManaMultiplier > 1 && (
<div className="flex justify-between text-sm">
<span className="text-amber-400">Upgrade Mana Multiplier:</span>
<span className="text-amber-300">×{fmtDec(upgradeEffects.maxManaMultiplier, 2)}</span>
</div>
)}
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2">
<span className="text-gray-300">Total Max Mana:</span>
<span className="text-blue-400">{fmt(maxMana)}</span>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Base Regen:</span>
<span className="text-gray-200">2/hr</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Mana Flow Bonus:</span>
<span className="text-blue-300">
{(() => {
const mf = 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>
{upgradeEffects.regenBonus > 0 && (
<div className="flex justify-between text-sm">
<span className="text-amber-400">Upgrade Regen Bonus:</span>
<span className="text-amber-300">+{fmtDec(upgradeEffects.regenBonus, 2)}/hr</span>
</div>
)}
{upgradeEffects.permanentRegenBonus > 0 && (
<div className="flex justify-between text-sm">
<span className="text-amber-400">Permanent Regen Bonus:</span>
<span className="text-amber-300">+{fmtDec(upgradeEffects.permanentRegenBonus, 2)}/hr</span>
</div>
)}
{upgradeEffects.regenMultiplier > 1 && (
<div className="flex justify-between text-sm">
<span className="text-amber-400">Upgrade Regen Multiplier:</span>
<span className="text-amber-300">×{fmtDec(upgradeEffects.regenMultiplier, 2)}</span>
</div>
)}
</div>
</div>
<Separator className="bg-gray-700 my-3" />
{upgradeEffects.activeUpgrades.length > 0 && (
<>
<div className="mb-2">
<span className="text-xs text-amber-400 game-panel-title">Active Skill Upgrades</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mb-3">
{upgradeEffects.activeUpgrades.map((upgrade, idx) => (
<div key={idx} className="flex justify-between text-xs bg-gray-800/50 rounded px-2 py-1">
<span className="text-gray-300">{upgrade.name}</span>
<span className="text-gray-400">{upgrade.desc}</span>
</div>
))}
</div>
<Separator className="bg-gray-700 my-3" />
</>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Click Mana Value:</span>
<span className="text-purple-300">+{clickMana}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Mana Tap Bonus:</span>
<span className="text-purple-300">+{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>
);
}
@@ -0,0 +1,55 @@
'use client';
import { fmtDec } from '@/lib/game/store';
import type { GameStore } from '@/lib/game/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { BookOpen } from 'lucide-react';
export interface StudyStatsSectionProps {
store: GameStore;
studySpeedMult: number;
studyCostMult: number;
}
export function StudyStatsSection({ store, studySpeedMult, studyCostMult }: StudyStatsSectionProps) {
return (
<Card className="bg-gray-900/80 border-gray-700">
<CardHeader className="pb-2">
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
<BookOpen className="w-4 h-4" />
Study Stats
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Study Speed:</span>
<span className="text-purple-300">×{fmtDec(studySpeedMult, 2)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Quick Learner Bonus:</span>
<span className="text-purple-300">+{((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>
);
}
@@ -0,0 +1,82 @@
'use client';
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
import { SKILLS_DEF } from '@/lib/game/constants';
import type { GameStore, SkillUpgradeChoice } from '@/lib/game/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Star } from 'lucide-react';
export interface UpgradeEffectsSectionProps {
store: GameStore;
}
// Helper function to get all selected skill upgrades
function getAllSelectedUpgrades(store: GameStore) {
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;
}
export function UpgradeEffectsSection({ store }: UpgradeEffectsSectionProps) {
const selectedUpgrades = getAllSelectedUpgrades(store);
return (
<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>
);
}
+11
View File
@@ -0,0 +1,11 @@
export { ManaStatsSection } from './ManaStatsSection';
export type { ManaStatsSectionProps } from './ManaStatsSection';
export { CombatStatsSection } from './CombatStatsSection';
export type { CombatStatsSectionProps } from './CombatStatsSection';
export { StudyStatsSection } from './StudyStatsSection';
export type { StudyStatsSectionProps } from './StudyStatsSection';
export { UpgradeEffectsSection } from './UpgradeEffectsSection';
export type { UpgradeEffectsSectionProps } from './UpgradeEffectsSection';