df67abca50
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m46s
- Fix game loop store mismatch (page.tsx now uses modular stores matching gameHooks.ts) - Fix StatsTab.tsx type errors (signedPacts now from usePrestigeStore) - Fix React hooks violations (all hooks called before conditional returns) - Add ErrorBoundary to page.tsx for better error handling - Fix getStudySpeedMultiplier called with correct arguments - Build succeeds consistently
324 lines
14 KiB
TypeScript
324 lines
14 KiB
TypeScript
'use client';
|
||
|
||
import { ELEMENTS, GUARDIANS } from '@/lib/game/constants';
|
||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||
import { fmt, fmtDec, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/stores';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Separator } from '@/components/ui/separator';
|
||
import { FlaskConical, Trophy, RotateCcw } from 'lucide-react';
|
||
import { ManaStatsSection } from '../stats/ManaStatsSection';
|
||
import { ManaTypeBreakdown } from '../stats/ManaTypeBreakdown';
|
||
import { CombatStatsSection } from '../stats/CombatStatsSection';
|
||
import { StudyStatsSection } from '../stats/StudyStatsSection';
|
||
import { UpgradeEffectsSection } from '../stats/UpgradeEffectsSection';
|
||
|
||
// Modular stores
|
||
import { useCombatStore, useManaStore, useSkillStore, usePrestigeStore, useGameStore } from '@/lib/game/stores';
|
||
|
||
export function StatsTab() {
|
||
// Get state from modular stores
|
||
const skills = useSkillStore((s) => s.skills);
|
||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||
|
||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||
const loopCount = usePrestigeStore((s) => s.loopCount);
|
||
const insight = usePrestigeStore((s) => s.insight);
|
||
const totalInsight = usePrestigeStore((s) => s.totalInsight);
|
||
const memorySlots = usePrestigeStore((s) => s.memorySlots);
|
||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||
|
||
const elements = useManaStore((s) => s.elements);
|
||
const totalManaGathered = useManaStore((s) => s.totalManaGathered);
|
||
const rawMana = useManaStore((s) => s.rawMana);
|
||
const meditateTicks = useManaStore((s) => s.meditateTicks);
|
||
|
||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
||
const spells = useCombatStore((s) => s.spells);
|
||
|
||
// Compute unified effects
|
||
const upgradeEffects = getUnifiedEffects({
|
||
skillUpgrades,
|
||
skillTiers,
|
||
equippedInstances: {},
|
||
equipmentInstances: {}
|
||
});
|
||
|
||
// Compute derived stats
|
||
const maxMana = computeMaxMana({
|
||
skills,
|
||
prestigeUpgrades,
|
||
skillUpgrades,
|
||
skillTiers
|
||
}, upgradeEffects);
|
||
|
||
const baseRegen = computeRegen({
|
||
skills,
|
||
prestigeUpgrades,
|
||
skillUpgrades,
|
||
skillTiers
|
||
}, upgradeEffects);
|
||
|
||
const clickMana = computeClickMana({
|
||
skills,
|
||
prestigeUpgrades,
|
||
skillUpgrades,
|
||
skillTiers
|
||
});
|
||
|
||
const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency);
|
||
|
||
const day = useGameStore((s) => s.day);
|
||
const hour = useGameStore((s) => s.hour);
|
||
const incursionStrength = getIncursionStrength(day, hour);
|
||
|
||
// Effective regen with incursion penalty
|
||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||
|
||
// Mana Cascade bonus
|
||
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
||
? Math.floor(maxMana / 100) * 0.1
|
||
: 0;
|
||
|
||
// Mana Waterfall bonus
|
||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
||
? Math.floor(maxMana / 100) * 0.25
|
||
: 0;
|
||
|
||
// Effective regen
|
||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||
|
||
// Get study speed/cost multipliers
|
||
const studySpeedMult = getStudySpeedMultiplier(skills);
|
||
const studyCostMult = getStudyCostMultiplier(skills);
|
||
|
||
// Check special effects
|
||
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
|
||
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
|
||
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
|
||
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
|
||
|
||
// Compute element max
|
||
const elemMax = (() => {
|
||
const ea = skillTiers?.elemAttune || 1;
|
||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
||
const tierMult = getTierMultiplier(tieredSkillId);
|
||
return 10 + level * 50 * tierMult + (prestigeUpgrades.elementalAttune || 0) * 25;
|
||
})();
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* Mana Stats */}
|
||
<ManaStatsSection
|
||
upgradeEffects={upgradeEffects}
|
||
maxMana={maxMana}
|
||
baseRegen={baseRegen}
|
||
clickMana={clickMana}
|
||
meditationMultiplier={meditationMultiplier}
|
||
effectiveRegen={effectiveRegen}
|
||
incursionStrength={incursionStrength}
|
||
manaCascadeBonus={manaCascadeBonus}
|
||
manaWaterfallBonus={manaWaterfallBonus}
|
||
hasManaWaterfall={hasManaWaterfall}
|
||
hasFlowSurge={hasFlowSurge}
|
||
hasManaOverflow={hasManaOverflow}
|
||
hasEternalFlow={hasEternalFlow}
|
||
/>
|
||
|
||
{/* Mana Type Breakdown */}
|
||
<ManaTypeBreakdown />
|
||
|
||
{/* Combat Stats */}
|
||
<CombatStatsSection />
|
||
|
||
{/* Study Stats */}
|
||
<StudyStatsSection
|
||
studySpeedMult={studySpeedMult}
|
||
studyCostMult={studyCostMult}
|
||
/>
|
||
|
||
{/* 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 = skillTiers?.elemAttune || 1;
|
||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
||
const tierMult = getTierMultiplier(tieredSkillId);
|
||
return `+${level * 50 * tierMult}`;
|
||
})()}
|
||
</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Prestige Attunement:</span>
|
||
<span className="text-green-300">+{(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(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 + (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(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 */}
|
||
<UpgradeEffectsSection />
|
||
|
||
{/* Enchantment Power */}
|
||
<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">
|
||
✨ Enchantment Power
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-gray-400">Enchantment Power:</span>
|
||
<span className="text-blue-300 font-[var(--font-mono)]">
|
||
{upgradeEffects?.enchantmentPowerMultiplier?.toFixed(2) || '1.0'}×
|
||
</span>
|
||
</div>
|
||
<p className="text-xs text-gray-500 mt-2">
|
||
Increases the power of all enchantments by {((upgradeEffects?.enchantmentPowerMultiplier || 1) - 1) * 100}%. Multiplier applied to all enchantment effects.
|
||
</p>
|
||
</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 ({signedPacts.length}/10)
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{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">
|
||
{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(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">{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(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(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">{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(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(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(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">{memorySlots}</div>
|
||
<div className="text-xs text-gray-400">Memory Slots</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
StatsTab.displayName = "StatsTab";
|