fix: complete store migration — fix all tab crashes and ghost field reads
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m22s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m22s
This commit is contained in:
@@ -1,152 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Sparkles, Swords, BookOpen, Target, FlaskConical, Cog, Hammer } from 'lucide-react';
|
||||
import type { GameAction } from '@/lib/game/types';
|
||||
|
||||
interface ActionButtonsProps {
|
||||
currentAction: GameAction;
|
||||
currentStudyTarget: { type: 'skill' | 'spell'; id: string; progress: number; required: number } | null;
|
||||
designProgress: { progress: number; required: number } | null;
|
||||
designProgress2: { progress: number; required: number } | null;
|
||||
preparationProgress: { progress: number; required: number } | null;
|
||||
applicationProgress: { progress: number; required: number } | null;
|
||||
equipmentCraftingProgress: { progress: number; required: number } | null;
|
||||
}
|
||||
|
||||
// Map action IDs to labels and icons
|
||||
const ACTION_CONFIG: Record<string, { label: string; icon: typeof Sparkles; color: string }> = {
|
||||
meditate: { label: 'Meditating', icon: Sparkles, color: 'text-blue-400' },
|
||||
climb: { label: 'Climbing', icon: Swords, color: 'text-green-400' },
|
||||
study: { label: 'Studying', icon: BookOpen, color: 'text-yellow-400' },
|
||||
design: { label: 'Designing Enchantment', icon: Target, color: 'text-purple-400' },
|
||||
prepare: { label: 'Preparing Equipment', icon: FlaskConical, color: 'text-purple-400' },
|
||||
enchant: { label: 'Enchanting', icon: Sparkles, color: 'text-purple-400' },
|
||||
craft: { label: 'Crafting Equipment', icon: Hammer, color: 'text-orange-400' },
|
||||
convert: { label: 'Converting Mana', icon: Cog, color: 'text-cyan-400' },
|
||||
};
|
||||
|
||||
function ProgressBar({ progress, required, label }: { progress: number; required: number; label?: string }) {
|
||||
const percentage = Math.min(100, (progress / required) * 100);
|
||||
return (
|
||||
<div className="mt-1">
|
||||
{label && <div className="text-xs text-gray-400 mb-0.5">{label}</div>}
|
||||
<div className="w-full bg-gray-700 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-blue-500 h-1.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ActionButtons({
|
||||
currentAction,
|
||||
currentStudyTarget,
|
||||
designProgress,
|
||||
designProgress2,
|
||||
preparationProgress,
|
||||
applicationProgress,
|
||||
equipmentCraftingProgress,
|
||||
}: ActionButtonsProps) {
|
||||
const config = ACTION_CONFIG[currentAction] || { label: currentAction, icon: Sparkles, color: 'text-gray-400' };
|
||||
const Icon = config.icon;
|
||||
|
||||
// Calculate additional info for specific actions
|
||||
const getActionDetails = () => {
|
||||
switch (currentAction) {
|
||||
case 'study':
|
||||
if (currentStudyTarget) {
|
||||
const progress = currentStudyTarget.progress;
|
||||
const required = currentStudyTarget.required;
|
||||
const percentage = Math.min(100, (progress / required) * 100);
|
||||
return (
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
required={required}
|
||||
label={`${currentStudyTarget.type === 'skill' ? 'Skill' : 'Spell'}: ${percentage.toFixed(0)}%`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'design':
|
||||
if (designProgress) {
|
||||
return (
|
||||
<ProgressBar
|
||||
progress={designProgress.progress}
|
||||
required={designProgress.required}
|
||||
label="Design progress"
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'prepare':
|
||||
if (preparationProgress) {
|
||||
return (
|
||||
<ProgressBar
|
||||
progress={preparationProgress.progress}
|
||||
required={preparationProgress.required}
|
||||
label="Preparation progress"
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'enchant':
|
||||
if (applicationProgress) {
|
||||
return (
|
||||
<ProgressBar
|
||||
progress={applicationProgress.progress}
|
||||
required={applicationProgress.required}
|
||||
label="Enchantment progress"
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'craft':
|
||||
if (equipmentCraftingProgress) {
|
||||
return (
|
||||
<ProgressBar
|
||||
progress={equipmentCraftingProgress.progress}
|
||||
required={equipmentCraftingProgress.required}
|
||||
label="Crafting progress"
|
||||
/>
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="bg-gray-800/50 rounded-lg p-3 border border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className={`w-4 h-4 ${config.color}`} />
|
||||
<span className="text-sm font-medium text-gray-200">Current Activity</span>
|
||||
</div>
|
||||
<div className={`text-lg font-semibold mt-1 ${config.color}`}>
|
||||
{config.label}
|
||||
</div>
|
||||
{getActionDetails()}
|
||||
|
||||
{/* Show second design slot if active */}
|
||||
{designProgress2 && (
|
||||
<div className="mt-2 pt-2 border-t border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<Target className="w-3 h-3 text-purple-400" />
|
||||
<span className="text-xs text-gray-400">Second Design Slot</span>
|
||||
</div>
|
||||
<ProgressBar
|
||||
progress={designProgress2.progress}
|
||||
required={designProgress2.required}
|
||||
label="Design progress"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ActionButtons.displayName = "ActionButtons";
|
||||
ProgressBar.displayName = "ProgressBar";
|
||||
@@ -1,13 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useGameStore } from '@/lib/game/store';
|
||||
import { useManaStore } from '@/lib/game/stores';
|
||||
import { ELEMENTS, MANA_PER_ELEMENT } from '@/lib/game/constants';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export function LabTab() {
|
||||
const store = useGameStore();
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const convertMana = useManaStore((s) => s.convertMana);
|
||||
const unlockElement = useManaStore((s) => s.unlockElement);
|
||||
const craftComposite = useManaStore((s) => s.craftComposite);
|
||||
|
||||
const [convertTarget, setConvertTarget] = useState('fire');
|
||||
|
||||
return (
|
||||
@@ -19,7 +24,7 @@ export function LabTab() {
|
||||
</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)
|
||||
{Object.entries(elements)
|
||||
.filter(([, state]) => state.unlocked && state.current >= 1)
|
||||
.map(([id, state]) => {
|
||||
const def = ELEMENTS[id];
|
||||
@@ -55,24 +60,24 @@ export function LabTab() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => store.convertMana(convertTarget, 1)}
|
||||
disabled={!store.elements[convertTarget]?.unlocked || store.rawMana < MANA_PER_ELEMENT}
|
||||
onClick={() => convertMana(convertTarget, 1)}
|
||||
disabled={!elements[convertTarget]?.unlocked || 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}
|
||||
onClick={() => convertMana(convertTarget, 10)}
|
||||
disabled={!elements[convertTarget]?.unlocked || 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}
|
||||
onClick={() => convertMana(convertTarget, 100)}
|
||||
disabled={!elements[convertTarget]?.unlocked || rawMana < MANA_PER_ELEMENT * 100}
|
||||
>
|
||||
+100 ({MANA_PER_ELEMENT * 100})
|
||||
</Button>
|
||||
@@ -91,7 +96,7 @@ export function LabTab() {
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{Object.entries(store.elements)
|
||||
{Object.entries(elements)
|
||||
.filter(([id, state]) => !state.unlocked && ELEMENTS[id]?.cat !== 'exotic')
|
||||
.map(([id]) => {
|
||||
const def = ELEMENTS[id];
|
||||
@@ -106,8 +111,8 @@ export function LabTab() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-1 w-full"
|
||||
disabled={store.rawMana < 500}
|
||||
onClick={() => store.unlockElement(id)}
|
||||
disabled={rawMana < 500}
|
||||
onClick={() => unlockElement(id, 500)}
|
||||
>
|
||||
Unlock
|
||||
</Button>
|
||||
@@ -128,10 +133,10 @@ export function LabTab() {
|
||||
{Object.entries(ELEMENTS)
|
||||
.filter(([, def]) => def.recipe)
|
||||
.map(([id, def]) => {
|
||||
const state = store.elements[id];
|
||||
const state = elements[id];
|
||||
const recipe = def.recipe!;
|
||||
const canCraft = recipe.every(
|
||||
(r) => (store.elements[r]?.current || 0) >= recipe.filter((x) => x === r).length
|
||||
(r) => (elements[r]?.current || 0) >= recipe.filter((x) => x === r).length
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -156,7 +161,7 @@ export function LabTab() {
|
||||
variant={canCraft ? 'default' : 'outline'}
|
||||
className="w-full"
|
||||
disabled={!canCraft}
|
||||
onClick={() => store.craftComposite(id)}
|
||||
onClick={() => craftComposite(id, recipe)}
|
||||
>
|
||||
Craft
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useGameStore, fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { useSkillStore, useManaStore, useCombatStore, fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { SKILLS_DEF, SKILL_CATEGORIES } from '@/lib/game/constants';
|
||||
import { getNextTierSkill, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { computeEffects } from '@/lib/game/upgrade-effects';
|
||||
@@ -16,24 +16,30 @@ interface SkillRowProps {
|
||||
}
|
||||
|
||||
export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
const store = useGameStore();
|
||||
const skillState = useSkillStore((s) => s);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const startStudyingSkill = useSkillStore((s) => s.startStudyingSkill);
|
||||
const tierUpSkill = useSkillStore((s) => s.tierUpSkill);
|
||||
const startParallelStudySkill = useSkillStore((s) => s.startParallelStudySkill);
|
||||
|
||||
const { studySpeedMult, studyCostMult, hasParallelStudy } = useStudyStats();
|
||||
|
||||
const skillInfo = getSkillDisplayInfo(store, skillId);
|
||||
const {
|
||||
currentTier,
|
||||
tieredSkillId,
|
||||
tierMultiplier,
|
||||
level,
|
||||
maxed,
|
||||
isStudying,
|
||||
skillDisplayName,
|
||||
prereqMet,
|
||||
def
|
||||
const skillInfo = getSkillDisplayInfo(skillState, skillId);
|
||||
const {
|
||||
currentTier,
|
||||
tieredSkillId,
|
||||
tierMultiplier,
|
||||
level,
|
||||
maxed,
|
||||
isStudying,
|
||||
savedProgress,
|
||||
skillDisplayName,
|
||||
prereqMet,
|
||||
def
|
||||
} = skillInfo;
|
||||
|
||||
// Apply skill modifiers
|
||||
const studyEffects = computeEffects(store.skillUpgrades || {}, store.skillTiers || {});
|
||||
const studyEffects = computeEffects(skillState.skillUpgrades || {}, skillState.skillTiers || {});
|
||||
const effectiveSpeedMult = studySpeedMult * studyEffects.studySpeedMultiplier;
|
||||
|
||||
const tierStudyTime = def.studyTime * currentTier;
|
||||
@@ -43,15 +49,16 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
const cost = Math.floor(baseCost * studyCostMult);
|
||||
|
||||
// Check if any study is in progress (prevent switching topics)
|
||||
const isAnyStudyInProgress = store.currentAction === 'study' && store.currentStudyTarget;
|
||||
const currentAction = useCombatStore((s) => s.currentAction);
|
||||
const isAnyStudyInProgress = currentAction === 'study' && skillState.currentStudyTarget;
|
||||
// Can only study if: not maxed, prereqs met, has mana, and either no study in progress or already studying this skill
|
||||
const canStudy = !maxed && prereqMet && store.rawMana >= cost && (!isAnyStudyInProgress || isStudying);
|
||||
const canStudy = !maxed && prereqMet && rawMana >= cost && (!isAnyStudyInProgress || isStudying);
|
||||
|
||||
const milestoneInfo = hasMilestoneUpgrade(store, tieredSkillId, level);
|
||||
const milestoneInfo = hasMilestoneUpgrade(skillState, tieredSkillId, level);
|
||||
const nextTierSkill = getNextTierSkill(tieredSkillId);
|
||||
const canTierUp = maxed && nextTierSkill;
|
||||
|
||||
const selectedUpgrades = store.skillUpgrades[tieredSkillId] || [];
|
||||
const selectedUpgrades = skillState.skillUpgrades[tieredSkillId] || [];
|
||||
const selectedL5 = selectedUpgrades.filter(u => u.includes('_l5'));
|
||||
const selectedL10 = selectedUpgrades.filter(u => u.includes('_l10'));
|
||||
|
||||
@@ -59,7 +66,7 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
<div
|
||||
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' :
|
||||
milestoneInfo ? 'border-amber-500/50 bg-amber-900/10' :
|
||||
'border-gray-700 bg-gray-800/30'
|
||||
}`}
|
||||
>
|
||||
@@ -84,7 +91,7 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
<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(', ')}
|
||||
Requires: {Object.entries(def.req).map(([r, rl]) => `${SKILLS_DEF[r]?.name || r} Lv.${rl}`).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
@@ -103,7 +110,7 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3 flex-wrap sm:flex-nowrap">
|
||||
{/* Level dots */}
|
||||
<div className="flex gap-1 shrink-0">
|
||||
@@ -118,10 +125,10 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
{isStudying ? (
|
||||
<div className="text-xs text-purple-400">
|
||||
{formatStudyTime(store.currentStudyTarget?.progress || 0)}/{formatStudyTime(tierStudyTime)}
|
||||
{formatStudyTime(savedProgress || 0)}/{formatStudyTime(tierStudyTime)}
|
||||
</div>
|
||||
) : milestoneInfo ? (
|
||||
<Button
|
||||
@@ -137,7 +144,7 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={() => store.tierUpSkill(tieredSkillId)}
|
||||
onClick={() => tierUpSkill(tieredSkillId)}
|
||||
>
|
||||
⬆️ Tier Up
|
||||
</Button>
|
||||
@@ -153,23 +160,23 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
variant={canStudy ? 'default' : 'outline'}
|
||||
disabled={!canStudy}
|
||||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||||
onClick={() => store.startStudyingSkill(tieredSkillId)}
|
||||
onClick={() => startStudyingSkill(tieredSkillId)}
|
||||
>
|
||||
Study ({fmt(cost)})
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
{!canStudy && isAnyStudyInProgress && !isStudying && (
|
||||
<TooltipContent>
|
||||
<p>Cannot switch topics while studying {SKILLS_DEF[store.currentStudyTarget?.id || '']?.name || 'another skill'}</p>
|
||||
<p>Cannot switch topics while studying {SKILLS_DEF[skillState.currentStudyTarget?.id || '']?.name || 'another skill'}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{/* Parallel Study button */}
|
||||
{hasParallelStudy &&
|
||||
store.currentStudyTarget &&
|
||||
!store.parallelStudyTarget &&
|
||||
store.currentStudyTarget.id !== tieredSkillId &&
|
||||
skillState.currentStudyTarget &&
|
||||
!skillState.parallelStudyTarget &&
|
||||
skillState.currentStudyTarget.id !== tieredSkillId &&
|
||||
canStudy && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -178,7 +185,7 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
||||
onClick={() => store.startParallelStudySkill(tieredSkillId)}
|
||||
onClick={() => startParallelStudySkill(tieredSkillId)}
|
||||
>
|
||||
⚡
|
||||
</Button>
|
||||
@@ -195,3 +202,5 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SkillRow.displayName = "SkillRow";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getUpgradesForSkillAtMilestone, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import type { GameStore } from '@/lib/game/store';
|
||||
import type { SkillState } from '@/lib/game/stores';
|
||||
|
||||
// Format study time
|
||||
export function formatStudyTime(hours: number): string {
|
||||
@@ -8,9 +8,9 @@ export function formatStudyTime(hours: number): string {
|
||||
return `${hours.toFixed(1)}h`;
|
||||
}
|
||||
|
||||
// Check if skill has milestone available
|
||||
// Check if skill has milestone upgrade available
|
||||
export function hasMilestoneUpgrade(
|
||||
store: GameStore,
|
||||
skillState: SkillState,
|
||||
skillId: string,
|
||||
level: number
|
||||
): { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null {
|
||||
@@ -19,16 +19,16 @@ export function hasMilestoneUpgrade(
|
||||
if (!path) return null;
|
||||
|
||||
if (level >= 5) {
|
||||
const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, store.skillTiers);
|
||||
const selected5 = (store.skillUpgrades[skillId] || []).filter(id => id.includes('_l5'));
|
||||
const upgrades5 = getUpgradesForSkillAtMilestone(skillId, 5, skillState.skillTiers);
|
||||
const selected5 = (skillState.skillUpgrades[skillId] || []).filter(id => id.includes('_l5'));
|
||||
if (upgrades5.length > 0 && selected5.length < 2) {
|
||||
return { milestone: 5, hasUpgrades: true, selectedCount: selected5.length };
|
||||
}
|
||||
}
|
||||
|
||||
if (level >= 10) {
|
||||
const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, store.skillTiers);
|
||||
const selected10 = (store.skillUpgrades[skillId] || []).filter(id => id.includes('_l10'));
|
||||
const upgrades10 = getUpgradesForSkillAtMilestone(skillId, 10, skillState.skillTiers);
|
||||
const selected10 = (skillState.skillUpgrades[skillId] || []).filter(id => id.includes('_l10'));
|
||||
if (upgrades10.length > 0 && selected10.length < 2) {
|
||||
return { milestone: 10, hasUpgrades: true, selectedCount: selected10.length };
|
||||
}
|
||||
@@ -39,28 +39,28 @@ export function hasMilestoneUpgrade(
|
||||
|
||||
// Get skill display info
|
||||
export function getSkillDisplayInfo(
|
||||
store: GameStore,
|
||||
skillState: SkillState,
|
||||
skillId: string
|
||||
) {
|
||||
const currentTier = store.skillTiers?.[skillId] || 1;
|
||||
const currentTier = skillState.skillTiers?.[skillId] || 1;
|
||||
const tieredSkillId = currentTier > 1 ? `${skillId}_t${currentTier}` : skillId;
|
||||
const def = SKILLS_DEF[skillId];
|
||||
const tierMultiplier = getTierMultiplier(tieredSkillId);
|
||||
|
||||
const level = store.skills[tieredSkillId] || store.skills[skillId] || 0;
|
||||
const maxed = level >= def.max;
|
||||
const level = skillState.skills[tieredSkillId] || skillState.skills[skillId] || 0;
|
||||
const maxed = level >= (def?.max || 10);
|
||||
|
||||
const isStudying = (store.currentStudyTarget?.id === skillId || store.currentStudyTarget?.id === tieredSkillId) && store.currentStudyTarget?.type === 'skill';
|
||||
const savedProgress = store.skillProgress[tieredSkillId] || store.skillProgress[skillId] || 0;
|
||||
const isStudying = (skillState.currentStudyTarget?.id === skillId || skillState.currentStudyTarget?.id === tieredSkillId) && skillState.currentStudyTarget?.type === 'skill';
|
||||
const savedProgress = skillState.skillProgress[tieredSkillId] || skillState.skillProgress[skillId] || 0;
|
||||
|
||||
const tierDef = SKILL_EVOLUTION_PATHS[skillId]?.tiers.find(t => t.tier === currentTier);
|
||||
const skillDisplayName = tierDef?.name || def.name;
|
||||
const skillDisplayName = tierDef?.name || def?.name || skillId;
|
||||
|
||||
// Check prerequisites
|
||||
let prereqMet = true;
|
||||
if (def.req) {
|
||||
if (def?.req) {
|
||||
for (const [r, rl] of Object.entries(def.req)) {
|
||||
if ((store.skills[r] || 0) < rl) {
|
||||
if ((skillState.skills[r] || 0) < rl) {
|
||||
prereqMet = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useGameStore, fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { useSkillStore, usePrestigeStore, fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { SKILL_EVOLUTION_PATHS, getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
||||
@@ -14,31 +14,35 @@ import { LoopStatsSection } from './StatsTab/LoopStatsSection';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
|
||||
export function StatsTab() {
|
||||
const store = useGameStore();
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
|
||||
const manaStats = useManaStats();
|
||||
const combatStats = useCombatStats();
|
||||
const studyStats = useStudyStats();
|
||||
|
||||
// Compute element max
|
||||
const elemMax = (() => {
|
||||
const ea = store.skillTiers?.elemAttune || 1;
|
||||
const ea = skillTiers?.elemAttune || 1;
|
||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||
const level = store.skills[tieredSkillId] || store.skills.elemAttune || 0;
|
||||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return 10 + level * 50 * tierMult + (store.prestigeUpgrades.elementalAttune || 0) * 25;
|
||||
return 10 + level * 50 * tierMult + (prestigeUpgrades.elementalAttune || 0) * 25;
|
||||
})();
|
||||
|
||||
// Get all selected skill upgrades
|
||||
const getAllSelectedUpgrades = (): { skillId: string; upgrade: SkillUpgradeChoice }[] => {
|
||||
const upgrades: { skillId: string; upgrade: SkillUpgradeChoice }[] = [];
|
||||
for (const [skillId, selectedIds] of Object.entries(store.skillUpgrades)) {
|
||||
for (const [skillId, selectedIds] of Object.entries(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 as any).upgrades?.find(u => u.id === upgradeId);
|
||||
const upgrade = (tier as any).upgrades?.find((u: any) => u.id === upgradeId);
|
||||
if (upgrade) {
|
||||
upgrades.push({ skillId, upgrade });
|
||||
}
|
||||
@@ -60,31 +64,26 @@ export function StatsTab() {
|
||||
clickMana={manaStats.clickMana}
|
||||
meditationMultiplier={manaStats.meditationMultiplier}
|
||||
upgradeEffects={manaStats.upgradeEffects}
|
||||
store={store}
|
||||
elemMax={elemMax}
|
||||
selectedUpgrades={selectedUpgrades}
|
||||
/>
|
||||
<CombatStatsSection
|
||||
store={store}
|
||||
activeSpellDef={combatStats.activeSpellDef}
|
||||
pactMultiplier={combatStats.pactMultiplier}
|
||||
/>
|
||||
<PactStatusSection
|
||||
store={store}
|
||||
pactMultiplier={combatStats.pactMultiplier}
|
||||
pactInsightMultiplier={combatStats.pactInsightMultiplier}
|
||||
/>
|
||||
<StudyStatsSection
|
||||
studySpeedMult={studyStats.studySpeedMult}
|
||||
studyCostMult={studyStats.studyCostMult}
|
||||
store={store}
|
||||
/>
|
||||
<ElementStatsSection
|
||||
store={store}
|
||||
elemMax={elemMax}
|
||||
/>
|
||||
<ActiveUpgradesSection selectedUpgrades={selectedUpgrades} />
|
||||
<LoopStatsSection store={store} />
|
||||
<LoopStatsSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,18 +2,26 @@
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Swords } from 'lucide-react';
|
||||
import type { GameStore } from '@/lib/game/stores';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
|
||||
interface CombatStatsSectionProps {
|
||||
store: GameStore;
|
||||
activeSpellDef: any;
|
||||
pactMultiplier: number;
|
||||
}
|
||||
|
||||
export function CombatStatsSection({ store, activeSpellDef, pactMultiplier }: CombatStatsSectionProps) {
|
||||
const upgradeEffects = getUnifiedEffects(store);
|
||||
export function CombatStatsSection({ activeSpellDef, pactMultiplier }: CombatStatsSectionProps) {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
|
||||
const upgradeEffects = getUnifiedEffects({
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
equippedInstances: {},
|
||||
equipmentInstances: {},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
@@ -32,25 +40,25 @@ export function CombatStatsSection({ store, activeSpellDef, pactMultiplier }: Co
|
||||
</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>
|
||||
<span className="text-red-300">+{(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>
|
||||
<span className="text-red-300">×{fmtDec(1 + (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>
|
||||
<span className="text-red-300">×{fmtDec(1 + (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>
|
||||
<span className="text-red-300">×{fmtDec(1 + (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>
|
||||
<span className="text-amber-300">{((skills.precision || 0) * 5)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Critical Multiplier:</span>
|
||||
@@ -58,7 +66,7 @@ export function CombatStatsSection({ store, activeSpellDef, pactMultiplier }: Co
|
||||
</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>
|
||||
<span className="text-amber-300">{((skills.spellEcho || 0) * 10)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Pact Multiplier:</span>
|
||||
@@ -66,7 +74,7 @@ export function CombatStatsSection({ store, activeSpellDef, pactMultiplier }: Co
|
||||
</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(store.activeSpell ? activeSpellDef?.dmg * pactMultiplier : 0)}</span>
|
||||
<span className="text-red-400">{fmt(activeSpellDef ? activeSpellDef.dmg * pactMultiplier : 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,21 +6,26 @@ import { FlaskConical } from 'lucide-react';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { useSkillStore, usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
|
||||
interface ElementStatsSectionProps {
|
||||
store: any;
|
||||
elemMax: number;
|
||||
}
|
||||
|
||||
export function ElementStatsSection({ store, elemMax }: ElementStatsSectionProps) {
|
||||
export function ElementStatsSection({ elemMax }: ElementStatsSectionProps) {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
|
||||
const getElemAttunementBonus = () => {
|
||||
const ea = store.skillTiers?.elemAttune || 1;
|
||||
const ea = skillTiers?.elemAttune || 1;
|
||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||
const level = store.skills[tieredSkillId] || store.skills.elemAttune || 0;
|
||||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return level * 50 * tierMult;
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -42,24 +47,24 @@ export function ElementStatsSection({ store, elemMax }: ElementStatsSectionProps
|
||||
</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>
|
||||
<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(store.elements || {}).filter((e: any) => e.unlocked).length} / {Object.keys(ELEMENTS).length}</span>
|
||||
<span className="text-green-300">{Object.values(elements || {}).filter((e: any) => 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>
|
||||
<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(store.elements)
|
||||
{Object.entries(elements)
|
||||
.filter(([, state]: [string, any]) => state.unlocked)
|
||||
.map(([id, state]: [string, any]) => {
|
||||
const def = ELEMENTS[id];
|
||||
|
||||
@@ -4,15 +4,21 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import { useCombatStore, useSkillStore, usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
|
||||
interface LoopStatsSectionProps {
|
||||
store: any;
|
||||
}
|
||||
|
||||
export function LoopStatsSection({ store }: LoopStatsSectionProps) {
|
||||
const spellsLearned = Object.values(store.spells || {}).filter((s) => s.learned).length;
|
||||
const totalSkillLevels = Object.values(store.skills || {}).reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
export function LoopStatsSection() {
|
||||
const spells = useCombatStore((s) => s.spells);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const insight = usePrestigeStore((s) => s.insight);
|
||||
const totalInsight = usePrestigeStore((s) => s.totalInsight);
|
||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
||||
const totalManaGathered = useManaStore((s) => s.totalManaGathered);
|
||||
const loopCount = usePrestigeStore((s) => s.loopCount);
|
||||
const memorySlots = useSkillStore((s) => s.memorySlots);
|
||||
|
||||
const spellsLearned = Object.values(spells || {}).filter((s: any) => s.learned).length;
|
||||
const totalSkillLevels = Object.values(skills || {}).reduce((a: number, b: number) => a + b, 0);
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -24,19 +30,19 @@ export function LoopStatsSection({ store }: LoopStatsSectionProps) {
|
||||
<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-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(store.insight)}</div>
|
||||
<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(store.totalInsight)}</div>
|
||||
<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">{store.maxFloorReached}</div>
|
||||
<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>
|
||||
@@ -51,11 +57,11 @@ export function LoopStatsSection({ store }: LoopStatsSectionProps) {
|
||||
<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-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">{store.memorySlots}</div>
|
||||
<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>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { fmt, fmtDec } from '@/lib/game/stores';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Droplet } from 'lucide-react';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { useSkillStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
interface ManaStatsSectionProps {
|
||||
maxMana: number;
|
||||
@@ -12,7 +13,6 @@ interface ManaStatsSectionProps {
|
||||
clickMana: number;
|
||||
meditationMultiplier: number;
|
||||
upgradeEffects: any;
|
||||
store: any;
|
||||
elemMax: number;
|
||||
selectedUpgrades: { skillId: string; upgrade: SkillUpgradeChoice }[];
|
||||
}
|
||||
@@ -24,13 +24,15 @@ export function ManaStatsSection({
|
||||
clickMana,
|
||||
meditationMultiplier,
|
||||
upgradeEffects,
|
||||
store,
|
||||
elemMax,
|
||||
selectedUpgrades,
|
||||
}: ManaStatsSectionProps) {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
|
||||
const getTierMultiplier = (skillId: string) => {
|
||||
// Simplified - import from skill-evolution in real implementation
|
||||
return 1;
|
||||
return 1; // Simplified - import from skill-evolution in real implementation
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -52,9 +54,9 @@ export function ManaStatsSection({
|
||||
<span className="text-gray-400">Mana Well Bonus:</span>
|
||||
<span className="text-blue-300">
|
||||
{(() => {
|
||||
const mw = store.skillTiers?.manaWell || 1;
|
||||
const mw = skillTiers?.manaWell || 1;
|
||||
const tieredSkillId = mw > 1 ? `manaWell_t${mw}` : 'manaWell';
|
||||
const level = store.skills[tieredSkillId] || store.skills.manaWell || 0;
|
||||
const level = skills[tieredSkillId] || skills.manaWell || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${fmt(level * 100 * tierMult)} (${level} lvl × 100 × ${tierMult}x tier)`;
|
||||
})()}
|
||||
@@ -62,7 +64,7 @@ export function ManaStatsSection({
|
||||
</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>
|
||||
<span className="text-blue-300">+{fmt((prestigeUpgrades.manaWell || 0) * 500)}</span>
|
||||
</div>
|
||||
{upgradeEffects.maxManaBonus > 0 && (
|
||||
<div className="flex justify-between text-sm">
|
||||
@@ -90,25 +92,25 @@ export function ManaStatsSection({
|
||||
<span className="text-gray-400">Mana Flow Bonus:</span>
|
||||
<span className="text-blue-300">
|
||||
{(() => {
|
||||
const mf = store.skillTiers?.manaFlow || 1;
|
||||
const mf = skillTiers?.manaFlow || 1;
|
||||
const tieredSkillId = mf > 1 ? `manaFlow_t${mf}` : 'manaFlow';
|
||||
const level = store.skills[tieredSkillId] || store.skills.manaFlow || 0;
|
||||
const level = skills[tieredSkillId] || skills.manaFlow || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${fmtDec(level * 1 * tierMult)}/hr (${level} lvl × 1 × ${tierMult}x tier)`;
|
||||
return `+${fmtDec(level * 1 * tierMult, 2)}/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>
|
||||
<span className="text-blue-300">+{(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>
|
||||
<span className="text-blue-300">+{fmtDec((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>
|
||||
<span className="text-blue-300">×{fmtDec(1 + (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>
|
||||
@@ -142,15 +144,15 @@ export function ManaStatsSection({
|
||||
</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>
|
||||
<span className="text-purple-300">+{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>
|
||||
<span className="text-purple-300">+{(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>
|
||||
<span className="text-purple-300">×{fmtDec(1 + (skills.manaOverflow || 0) * 0.25, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -4,15 +4,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Trophy } from 'lucide-react';
|
||||
import { fmtDec } from '@/lib/game/stores';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
|
||||
interface PactStatusSectionProps {
|
||||
store: any;
|
||||
pactMultiplier: number;
|
||||
pactInsightMultiplier: number;
|
||||
}
|
||||
|
||||
export function PactStatusSection({ store, pactMultiplier, pactInsightMultiplier }: PactStatusSectionProps) {
|
||||
const pactInterferenceMitigation = store.pactInterferenceMitigation || 0;
|
||||
export function PactStatusSection({ pactMultiplier, pactInsightMultiplier }: PactStatusSectionProps) {
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
|
||||
const pactInterferenceMitigation = prestigeUpgrades?.pactInterferenceMitigation || 0;
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
@@ -27,7 +31,7 @@ export function PactStatusSection({ store, pactMultiplier, pactInsightMultiplier
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Pact Slots:</span>
|
||||
<span className="text-amber-300">{store.signedPacts.length} / {1 + (store.prestigeUpgrades.pactCapacity || 0)}</span>
|
||||
<span className="text-amber-300">{signedPacts.length} / {1 + (prestigeUpgrades.pactCapacity || 0)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Damage Multiplier:</span>
|
||||
@@ -37,7 +41,7 @@ export function PactStatusSection({ store, pactMultiplier, pactInsightMultiplier
|
||||
<span className="text-gray-400">Insight Multiplier:</span>
|
||||
<span className="text-purple-300">×{fmtDec(pactInsightMultiplier, 2)}</span>
|
||||
</div>
|
||||
{store.signedPacts.length > 1 && (
|
||||
{signedPacts.length > 1 && (
|
||||
<>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Interference Mitigation:</span>
|
||||
@@ -55,8 +59,8 @@ export function PactStatusSection({ store, pactMultiplier, pactInsightMultiplier
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-gray-400 mb-2">Unlocked Mana Types:</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{Object.keys(store.elements).map((id) => {
|
||||
const state = store.elements[id];
|
||||
{Object.keys(elements).map((id) => {
|
||||
const state = elements[id];
|
||||
if (!state.unlocked) return null;
|
||||
const elem = ELEMENTS[id];
|
||||
return (
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import { fmtDec } from '@/lib/game/stores';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
|
||||
interface StudyStatsSectionProps {
|
||||
studySpeedMult: number;
|
||||
studyCostMult: number;
|
||||
store: any;
|
||||
}
|
||||
|
||||
export function StudyStatsSection({ studySpeedMult, studyCostMult, store }: StudyStatsSectionProps) {
|
||||
export function StudyStatsSection({ studySpeedMult, studyCostMult }: StudyStatsSectionProps) {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -28,7 +30,7 @@ export function StudyStatsSection({ studySpeedMult, studyCostMult, store }: Stud
|
||||
</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>
|
||||
<span className="text-purple-300">+{((skills.quickLearner || 0) * 10)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -38,13 +40,13 @@ export function StudyStatsSection({ studySpeedMult, studyCostMult, store }: Stud
|
||||
</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>
|
||||
<span className="text-purple-300">-{((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>
|
||||
<span className="text-purple-300">{Math.round((1 + (skills.knowledgeRetention || 0) * 0.2) * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,23 +4,25 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Sparkles, Unlock } from 'lucide-react';
|
||||
import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements';
|
||||
import { useGameStore } from '@/lib/game/store';
|
||||
import { usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
export function AttunementDebug() {
|
||||
const store = useGameStore((s) => s);
|
||||
|
||||
const attunements = usePrestigeStore((s) => s.attunements);
|
||||
const debugUnlockAttunement = usePrestigeStore((s) => s.debugUnlockAttunement);
|
||||
const debugAddAttunementXP = usePrestigeStore((s) => s.debugAddAttunementXP);
|
||||
|
||||
const handleUnlockAttunement = (id: string) => {
|
||||
if (useGameStore.getState().debugUnlockAttunement) {
|
||||
useGameStore.getState().debugUnlockAttunement(id);
|
||||
if (debugUnlockAttunement) {
|
||||
debugUnlockAttunement(id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleAddAttunementXP = (id: string, amount: number) => {
|
||||
if (useGameStore.getState().debugAddAttunementXP) {
|
||||
useGameStore.getState().debugAddAttunementXP(id, amount);
|
||||
if (debugAddAttunementXP) {
|
||||
debugAddAttunementXP(id, amount);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -31,10 +33,10 @@ export function AttunementDebug() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Object.entries(ATTUNEMENTS_DEF).map(([id, def]) => {
|
||||
const isActive = store.attunements?.[id]?.active;
|
||||
const level = store.attunements?.[id]?.level || 1;
|
||||
const xp = store.attunements?.[id]?.experience || 0;
|
||||
|
||||
const isActive = attunements?.[id]?.active;
|
||||
const level = attunements?.[id]?.level || 1;
|
||||
const xp = attunements?.[id]?.experience || 0;
|
||||
|
||||
return (
|
||||
<div key={id} className="flex items-center justify-between p-2 bg-gray-800/50 rounded">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -3,21 +3,24 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Star, Lock } from 'lucide-react';
|
||||
import { useGameStore } from '@/lib/game/store';
|
||||
import { useManaStore } from '@/lib/game/stores';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
|
||||
export function ElementDebug() {
|
||||
const store = useGameStore((s) => s);
|
||||
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const unlockElement = useManaStore((s) => s.unlockElement);
|
||||
const debugAddElementalMana = useManaStore((s) => s.debugAddElementalMana);
|
||||
|
||||
const handleUnlockElement = (element: string) => {
|
||||
useGameStore.getState().unlockElement(element);
|
||||
unlockElement(element, 500);
|
||||
};
|
||||
|
||||
|
||||
const handleAddElementalMana = (element: string, amount: number) => {
|
||||
if (useGameStore.getState().debugAddElementalMana) {
|
||||
useGameStore.getState().debugAddElementalMana(element, amount);
|
||||
if (debugAddElementalMana) {
|
||||
debugAddElementalMana(element, amount);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
@@ -28,16 +31,16 @@ export function ElementDebug() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
|
||||
{Object.entries(store.elements).map(([id, elem]) => {
|
||||
{Object.entries(elements).map(([id, elem]) => {
|
||||
const def = ELEMENTS[id];
|
||||
return (
|
||||
<div
|
||||
<div
|
||||
key={id}
|
||||
className={`p-2 rounded border text-center ${
|
||||
elem.unlocked ? 'border-gray-600 bg-gray-800/50' : 'border-gray-800 opacity-60'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: elem.unlocked ? def?.color : undefined
|
||||
style={{
|
||||
borderColor: elem.unlocked ? def?.color : undefined
|
||||
}}
|
||||
>
|
||||
<div className="text-lg">{def?.sym}</div>
|
||||
|
||||
@@ -10,40 +10,52 @@ import {
|
||||
RotateCcw, AlertTriangle, Zap, Clock, Settings, Eye,
|
||||
} from 'lucide-react';
|
||||
import { useDebug } from '@/lib/game/debug-context';
|
||||
import { useGameStore } from '@/lib/game/store';
|
||||
import { useGameStore, useManaStore } from '@/lib/game/stores';
|
||||
import { computeMaxMana } from '@/lib/game/stores';
|
||||
|
||||
export function GameStateDebug() {
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
const { showComponentNames, toggleComponentNames } = useDebug();
|
||||
|
||||
// Get state from store
|
||||
const store = useGameStore((s) => s);
|
||||
// Get state from modular stores
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const unlockElement = useManaStore((s) => s.unlockElement);
|
||||
const gatherMana = useGameStore((s) => s.gatherMana);
|
||||
const day = useGameStore((s) => s.day);
|
||||
const hour = useGameStore((s) => s.hour);
|
||||
const paused = useGameStore((s) => s.paused);
|
||||
const togglePause = useGameStore((s) => s.togglePause);
|
||||
|
||||
// Get actions from stores
|
||||
const resetGame = useGameStore((s) => s.resetGame);
|
||||
const setTime = useGameStore((s) => s.debugSetTime);
|
||||
const setFloor = useGameStore((s) => s.debugSetFloor);
|
||||
const resetHP = useGameStore((s) => s.resetFloorHP);
|
||||
|
||||
const handleReset = () => {
|
||||
if (confirmReset) {
|
||||
useGameStore.getState().resetGame();
|
||||
resetGame();
|
||||
setConfirmReset(false);
|
||||
} else {
|
||||
setConfirmReset(true);
|
||||
setTimeout(() => setConfirmReset(false), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleAddMana = (amount: number) => {
|
||||
// Use gatherMana multiple times to add mana
|
||||
const state = useGameStore.getState();
|
||||
for (let i = 0; i < amount; i++) {
|
||||
state.gatherMana();
|
||||
gatherMana();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetTime = (day: number, hour: number) => {
|
||||
const state = useGameStore.getState();
|
||||
if (state.debugSetTime) {
|
||||
state.debugSetTime(day, hour);
|
||||
}
|
||||
|
||||
const getMaxMana = () => {
|
||||
return computeMaxMana(
|
||||
{ skills: {}, prestigeUpgrades: {}, skillUpgrades: {}, skillTiers: {} },
|
||||
{ maxManaBonus: 0, maxManaMultiplier: 1 }
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Warning Banner */}
|
||||
@@ -126,7 +138,7 @@ export function GameStateDebug() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-xs text-gray-400 mb-2">
|
||||
Current: {store.rawMana} / {store.getMaxMana?.() || '?'}
|
||||
Current: {rawMana} / {getMaxMana() || '?'}
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(10)}>
|
||||
@@ -143,15 +155,15 @@ export function GameStateDebug() {
|
||||
</Button>
|
||||
</div>
|
||||
<Separator className="bg-gray-700" />
|
||||
<div className="text-xs text-gray-400">Fill to max:</div>
|
||||
<div className="text-xs text-gray-400 mb-2">Fill to max:</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
onClick={() => {
|
||||
const max = store.getMaxMana?.() || 100;
|
||||
const current = store.rawMana;
|
||||
const max = getMaxMana() || 100;
|
||||
const current = rawMana;
|
||||
for (let i = 0; i < Math.floor(max - current); i++) {
|
||||
store.gatherMana();
|
||||
gatherMana();
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -170,30 +182,30 @@ export function GameStateDebug() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-xs text-gray-400">
|
||||
Current: Day {store.day}, Hour {store.hour}
|
||||
Current: Day {day}, Hour {hour}
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button size="sm" variant="outline" onClick={() => handleSetTime(1, 0)}>
|
||||
<Button size="sm" variant="outline" onClick={() => setTime?.(1, 0)}>
|
||||
Day 1
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSetTime(10, 0)}>
|
||||
<Button size="sm" variant="outline" onClick={() => setTime?.(10, 0)}>
|
||||
Day 10
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSetTime(20, 0)}>
|
||||
<Button size="sm" variant="outline" onClick={() => setTime?.(20, 0)}>
|
||||
Day 20
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSetTime(30, 0)}>
|
||||
<Button size="sm" variant="outline" onClick={() => setTime?.(30, 0)}>
|
||||
Day 30
|
||||
</Button>
|
||||
</div>
|
||||
<Separator className="bg-gray-700" />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => useGameStore.getState().togglePause()}
|
||||
onClick={togglePause}
|
||||
>
|
||||
{store.paused ? '▶ Resume' : '⏸ Pause'}
|
||||
{paused ? '▶ Resume' : '⏸ Pause'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -215,8 +227,8 @@ export function GameStateDebug() {
|
||||
onClick={() => {
|
||||
// Unlock all base elements
|
||||
['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'].forEach(e => {
|
||||
if (!store.elements[e]?.unlocked) {
|
||||
useGameStore.getState().unlockElement(e);
|
||||
if (!elements[e]?.unlocked) {
|
||||
unlockElement(e, 500);
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -229,8 +241,8 @@ export function GameStateDebug() {
|
||||
onClick={() => {
|
||||
// Unlock utility elements
|
||||
['transference'].forEach(e => {
|
||||
if (!store.elements[e]?.unlocked) {
|
||||
useGameStore.getState().unlockElement(e);
|
||||
if (!elements[e]?.unlocked) {
|
||||
unlockElement(e, 500);
|
||||
}
|
||||
});
|
||||
}}
|
||||
@@ -240,24 +252,14 @@ export function GameStateDebug() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Max floor
|
||||
if (store.debugSetFloor) {
|
||||
useGameStore.getState().debugSetFloor(100);
|
||||
}
|
||||
}}
|
||||
onClick={() => setFloor?.(100)}
|
||||
>
|
||||
Skip to Floor 100
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Reset floor HP
|
||||
if (store.resetFloorHP) {
|
||||
useGameStore.getState().resetFloorHP();
|
||||
}
|
||||
}}
|
||||
onClick={() => resetHP?.()}
|
||||
>
|
||||
Reset Floor HP
|
||||
</Button>
|
||||
|
||||
@@ -4,10 +4,12 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { BookOpen } from 'lucide-react';
|
||||
import { useGameStore } from '@/lib/game/store';
|
||||
import { useSkillStore, useGameStore } from '@/lib/game/stores';
|
||||
|
||||
export function SkillDebug() {
|
||||
const store = useGameStore((s) => s);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const setSkills = useSkillStore.setState;
|
||||
const setGameState = useGameStore.setState;
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
|
||||
@@ -28,11 +30,13 @@ export function SkillDebug() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Level up all enchanting skills by 1
|
||||
const enchantSkills = ['enchanting', 'efficientEnchant', 'enchantSpeed','essenceRefining'];
|
||||
enchantSkills.forEach(skillId => {
|
||||
store.skills[skillId] = Math.min((store.skills[skillId] || 0) + 1, 10);
|
||||
// Force update
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
setSkills((prev) => {
|
||||
const enchantSkills = ['enchanting', 'efficientEnchant', 'enchantSpeed', 'essenceRefining'];
|
||||
const newSkills = { ...prev.skills };
|
||||
enchantSkills.forEach(skillId => {
|
||||
newSkills[skillId] = Math.min((newSkills[skillId] || 0) + 1, 10);
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -43,11 +47,14 @@ export function SkillDebug() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Max all enchanting skills
|
||||
const enchantSkills = ['enchanting', 'efficientEnchant', 'enchantSpeed','essenceRefining'];
|
||||
enchantSkills.forEach(skillId => {
|
||||
store.skills[skillId] = 10;
|
||||
setSkills((prev) => {
|
||||
const enchantSkills = ['enchanting', 'efficientEnchant', 'enchantSpeed', 'essenceRefining'];
|
||||
const newSkills = { ...prev.skills };
|
||||
enchantSkills.forEach(skillId => {
|
||||
newSkills[skillId] = 10;
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
}}
|
||||
>
|
||||
Max All Enchanting
|
||||
@@ -64,10 +71,13 @@ export function SkillDebug() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const manaSkills = ['manaWell', 'manaFlow', 'manaOverflow', 'fireManaCap', 'waterManaCap', 'airManaCap', 'earthManaCap', 'lightManaCap', 'darkManaCap', 'deathManaCap', 'metalManaCap', 'sandManaCap', 'lightningManaCap', 'transferenceManaCap'];
|
||||
manaSkills.forEach(skillId => {
|
||||
store.skills[skillId] = Math.min((store.skills[skillId] || 0) + 1, 10);
|
||||
setSkills((prev) => {
|
||||
const newSkills = { ...prev.skills };
|
||||
manaSkills.forEach(skillId => {
|
||||
newSkills[skillId] = Math.min((newSkills[skillId] || 0) + 1, 10);
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
}}
|
||||
>
|
||||
+1 All Mana
|
||||
@@ -77,10 +87,13 @@ export function SkillDebug() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const manaSkills = ['manaWell', 'manaFlow', 'manaOverflow', 'fireManaCap', 'waterManaCap', 'airManaCap', 'earthManaCap', 'lightManaCap', 'darkManaCap', 'deathManaCap', 'metalManaCap', 'sandManaCap', 'lightningManaCap', 'transferenceManaCap'];
|
||||
manaSkills.forEach(skillId => {
|
||||
store.skills[skillId] = 10;
|
||||
setSkills((prev) => {
|
||||
const newSkills = { ...prev.skills };
|
||||
manaSkills.forEach(skillId => {
|
||||
newSkills[skillId] = 10;
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
}}
|
||||
>
|
||||
Max All Mana
|
||||
@@ -97,10 +110,13 @@ export function SkillDebug() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const studySkills = ['quickLearner', 'focusedMind', 'meditation', 'knowledgeRetention'];
|
||||
studySkills.forEach(skillId => {
|
||||
store.skills[skillId] = Math.min((store.skills[skillId] || 0) + 1, 10);
|
||||
setSkills((prev) => {
|
||||
const newSkills = { ...prev.skills };
|
||||
studySkills.forEach(skillId => {
|
||||
newSkills[skillId] = Math.min((newSkills[skillId] || 0) + 1, 10);
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
}}
|
||||
>
|
||||
+1 All Study
|
||||
@@ -110,10 +126,13 @@ export function SkillDebug() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const studySkills = ['quickLearner', 'focusedMind', 'meditation', 'knowledgeRetention'];
|
||||
studySkills.forEach(skillId => {
|
||||
store.skills[skillId] = 10;
|
||||
setSkills((prev) => {
|
||||
const newSkills = { ...prev.skills };
|
||||
studySkills.forEach(skillId => {
|
||||
newSkills[skillId] = 10;
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
}}
|
||||
>
|
||||
Max All Study
|
||||
@@ -130,10 +149,13 @@ export function SkillDebug() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const craftSkills = ['effCrafting', 'fieldRepair', 'elemCrafting'];
|
||||
craftSkills.forEach(skillId => {
|
||||
store.skills[skillId] = Math.min((store.skills[skillId] || 0) + 1, 10);
|
||||
setSkills((prev) => {
|
||||
const newSkills = { ...prev.skills };
|
||||
craftSkills.forEach(skillId => {
|
||||
newSkills[skillId] = Math.min((newSkills[skillId] || 0) + 1, 10);
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
}}
|
||||
>
|
||||
+1 All Crafting
|
||||
@@ -143,10 +165,13 @@ export function SkillDebug() {
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const craftSkills = ['effCrafting', 'fieldRepair', 'elemCrafting'];
|
||||
craftSkills.forEach(skillId => {
|
||||
store.skills[skillId] = 10;
|
||||
setSkills((prev) => {
|
||||
const newSkills = { ...prev.skills };
|
||||
craftSkills.forEach(skillId => {
|
||||
newSkills[skillId] = 10;
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
}}
|
||||
>
|
||||
Max All Crafting
|
||||
@@ -162,21 +187,23 @@ export function SkillDebug() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Unlock all spell research
|
||||
const researchSkills = [
|
||||
'researchManaSpells', 'researchFireSpells', 'researchWaterSpells',
|
||||
'researchAirSpells', 'researchEarthSpells', 'researchLightSpells',
|
||||
'researchDarkSpells', 'researchLifeDeathSpells',
|
||||
'researchAdvancedFire', 'researchAdvancedWater', 'researchAdvancedAir',
|
||||
'researchAdvancedEarth', 'researchAdvancedLight', 'researchAdvancedDark',
|
||||
'researchMasterFire', 'researchMasterWater', 'researchMasterEarth',
|
||||
'researchDamageEffects', 'researchCombatEffects', 'researchManaEffects',
|
||||
'researchAdvancedManaEffects', 'researchUtilityEffects'
|
||||
];
|
||||
researchSkills.forEach(skillId => {
|
||||
store.skills[skillId] = 1;
|
||||
setSkills((prev) => {
|
||||
const researchSkills = [
|
||||
'researchManaSpells', 'researchFireSpells', 'researchWaterSpells',
|
||||
'researchAirSpells', 'researchEarthSpells', 'researchLightSpells',
|
||||
'researchDarkSpells', 'researchLifeDeathSpells',
|
||||
'researchAdvancedFire', 'researchAdvancedWater', 'researchAdvancedAir',
|
||||
'researchAdvancedEarth', 'researchAdvancedLight', 'researchAdvancedDark',
|
||||
'researchMasterFire', 'researchMasterWater', 'researchMasterEarth',
|
||||
'researchDamageEffects', 'researchCombatEffects', 'researchManaEffects',
|
||||
'researchAdvancedManaEffects', 'researchUtilityEffects'
|
||||
];
|
||||
const newSkills = { ...prev.skills };
|
||||
researchSkills.forEach(skillId => {
|
||||
newSkills[skillId] = 1;
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
useGameStore.setState({ skills: { ...store.skills } });
|
||||
}}
|
||||
>
|
||||
Unlock All Research
|
||||
@@ -185,47 +212,37 @@ export function SkillDebug() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Add all unlocked effects to unlockedEffects
|
||||
const effectIds = [
|
||||
// Spell effects
|
||||
'spell_manaBolt', 'spell_manaStrike', 'spell_fireball', 'spell_emberShot',
|
||||
'spell_waterJet', 'spell_iceShard', 'spell_gust', 'spell_windSlash',
|
||||
'spell_stoneBullet', 'spell_rockSpike', 'spell_lightLance', 'spell_radiance',
|
||||
'spell_shadowBolt', 'spell_darkPulse', 'spell_drain',
|
||||
// Tier 2 spells
|
||||
'spell_inferno', 'spell_flameWave', 'spell_tidalWave', 'spell_iceStorm',
|
||||
'spell_hurricane', 'spell_windBlade', 'spell_earthquake', 'spell_stoneBarrage',
|
||||
'spell_solarFlare', 'spell_divineSmite', 'spell_voidRift', 'spell_shadowStorm',
|
||||
// Tier 3 spells
|
||||
'spell_pyroclasm', 'spell_tsunami', 'spell_meteorStrike',
|
||||
// Lightning
|
||||
'spell_spark', 'spell_lightningBolt', 'spell_chainLightning',
|
||||
'spell_stormCall', 'spell_thunderStrike',
|
||||
// Metal and Sand
|
||||
'spell_metalShard', 'spell_ironFist', 'spell_steelTempest', 'spell_furnaceBlast',
|
||||
'spell_sandBlast', 'spell_sandstorm', 'spell_desertWind', 'spell_duneCollapse',
|
||||
// Mana effects
|
||||
'mana_cap_50', 'mana_cap_100', 'mana_regen_1', 'mana_regen_2', 'mana_regen_5',
|
||||
'click_mana_1', 'click_mana_3',
|
||||
// Combat effects
|
||||
'damage_5', 'damage_10', 'damage_pct_10', 'crit_5', 'attack_speed_10',
|
||||
// Utility effects
|
||||
'meditate_10', 'study_10', 'insight_5',
|
||||
// Special
|
||||
'spell_echo_10', 'guardian_dmg_10', 'overpower_80',
|
||||
// Weapon mana
|
||||
'weapon_mana_cap_20', 'weapon_mana_cap_50', 'weapon_mana_cap_100',
|
||||
'weapon_mana_regen_1', 'weapon_mana_regen_2', 'weapon_mana_regen_5',
|
||||
// Sword enchants
|
||||
'sword_fire', 'sword_frost', 'sword_lightning', 'sword_void'
|
||||
];
|
||||
const currentEffects = store.unlockedEffects || [];
|
||||
effectIds.forEach(id => {
|
||||
if (!currentEffects.includes(id)) {
|
||||
currentEffects.push(id);
|
||||
}
|
||||
setGameState((prev: any) => {
|
||||
const effectIds = [
|
||||
'spell_manaBolt', 'spell_manaStrike', 'spell_fireball', 'spell_emberShot',
|
||||
'spell_waterJet', 'spell_iceShard', 'spell_gust', 'spell_windSlash',
|
||||
'spell_stoneBullet', 'spell_rockSpike', 'spell_lightLance', 'spell_radiance',
|
||||
'spell_shadowBolt', 'spell_darkPulse', 'spell_drain',
|
||||
'spell_inferno', 'spell_flameWave', 'spell_tidalWave', 'spell_iceStorm',
|
||||
'spell_hurricane', 'spell_windBlade', 'spell_earthquake', 'spell_stoneBarrage',
|
||||
'spell_solarFlare', 'spell_divineSmite', 'spell_voidRift', 'spell_shadowStorm',
|
||||
'spell_pyroclasm', 'spell_tsunami', 'spell_meteorStrike',
|
||||
'spell_spark', 'spell_lightningBolt', 'spell_chainLightning',
|
||||
'spell_stormCall', 'spell_thunderStrike',
|
||||
'spell_metalShard', 'spell_ironFist', 'spell_steelTempest', 'spell_furnaceBlast',
|
||||
'spell_sandBlast', 'spell_sandstorm', 'spell_desertWind', 'spell_duneCollapse',
|
||||
'mana_cap_50', 'mana_cap_100', 'mana_regen_1', 'mana_regen_2', 'mana_regen_5',
|
||||
'click_mana_1', 'click_mana_3',
|
||||
'damage_5', 'damage_10', 'damage_pct_10', 'crit_5', 'attack_speed_10',
|
||||
'meditate_10', 'study_10', 'insight_5',
|
||||
'spell_echo_10', 'guardian_dmg_10', 'overpower_80',
|
||||
'weapon_mana_cap_20', 'weapon_mana_cap_50', 'weapon_mana_cap_100',
|
||||
'weapon_mana_regen_1', 'weapon_mana_regen_2', 'weapon_mana_regen_5',
|
||||
'sword_fire', 'sword_frost', 'sword_lightning', 'sword_void'
|
||||
];
|
||||
const currentEffects = prev.unlockedEffects || [];
|
||||
effectIds.forEach(id => {
|
||||
if (!currentEffects.includes(id)) {
|
||||
currentEffects.push(id);
|
||||
}
|
||||
});
|
||||
return { ...prev, unlockedEffects: currentEffects };
|
||||
});
|
||||
useGameStore.setState({ unlockedEffects: currentEffects });
|
||||
}}
|
||||
>
|
||||
Unlock All Effects
|
||||
@@ -240,33 +257,43 @@ export function SkillDebug() {
|
||||
size="sm"
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={() => {
|
||||
// Max all skills
|
||||
Object.keys(store.skills).forEach(skillId => {
|
||||
const current = store.skills[skillId] || 0;
|
||||
if (current < 10) {
|
||||
store.skills[skillId] = 10;
|
||||
}
|
||||
setSkills((prev) => {
|
||||
const allEffectIds = [
|
||||
'spell_manaBolt', 'spell_manaStrike', 'spell_fireball', 'spell_emberShot',
|
||||
'spell_waterJet', 'spell_iceShard', 'spell_gust', 'spell_windSlash',
|
||||
'spell_stoneBullet', 'spell_rockSpike', 'spell_lightLance', 'spell_radiance',
|
||||
'spell_shadowBolt', 'spell_darkPulse', 'spell_drain',
|
||||
'mana_cap_50', 'mana_cap_100', 'mana_regen_1', 'mana_regen_2', 'mana_regen_5',
|
||||
'click_mana_1', 'click_mana_3', 'damage_5', 'damage_10', 'damage_pct_10',
|
||||
'crit_5', 'attack_speed_10', 'meditate_10', 'study_10', 'insight_5',
|
||||
'spell_echo_10', 'guardian_dmg_10', 'overpower_80'
|
||||
];
|
||||
const newSkills = { ...prev.skills };
|
||||
Object.keys(newSkills).forEach(skillId => {
|
||||
if ((newSkills[skillId] || 0) < 10) {
|
||||
newSkills[skillId] = 10;
|
||||
}
|
||||
});
|
||||
return { ...prev, skills: newSkills };
|
||||
});
|
||||
// Unlock all effects
|
||||
const allEffectIds = [
|
||||
'spell_manaBolt', 'spell_manaStrike', 'spell_fireball', 'spell_emberShot',
|
||||
'spell_waterJet', 'spell_iceShard', 'spell_gust', 'spell_windSlash',
|
||||
'spell_stoneBullet', 'spell_rockSpike', 'spell_lightLance', 'spell_radiance',
|
||||
'spell_shadowBolt', 'spell_darkPulse', 'spell_drain',
|
||||
'mana_cap_50', 'mana_cap_100', 'mana_regen_1', 'mana_regen_2', 'mana_regen_5',
|
||||
'click_mana_1', 'click_mana_3', 'damage_5', 'damage_10', 'damage_pct_10',
|
||||
'crit_5', 'attack_speed_10', 'meditate_10', 'study_10', 'insight_5',
|
||||
'spell_echo_10', 'guardian_dmg_10', 'overpower_80'
|
||||
];
|
||||
const currentEffects = store.unlockedEffects || [];
|
||||
allEffectIds.forEach(id => {
|
||||
if (!currentEffects.includes(id)) {
|
||||
currentEffects.push(id);
|
||||
}
|
||||
});
|
||||
useGameStore.setState({
|
||||
skills: { ...store.skills },
|
||||
unlockedEffects: currentEffects
|
||||
setGameState((prev: any) => {
|
||||
const allEffectIds = [
|
||||
'spell_manaBolt', 'spell_manaStrike', 'spell_fireball', 'spell_emberShot',
|
||||
'spell_waterJet', 'spell_iceShard', 'spell_gust', 'spell_windSlash',
|
||||
'spell_stoneBullet', 'spell_rockSpike', 'spell_lightLance', 'spell_radiance',
|
||||
'spell_shadowBolt', 'spell_darkPulse', 'spell_drain',
|
||||
'mana_cap_50', 'mana_cap_100', 'mana_regen_1', 'mana_regen_2', 'mana_regen_5',
|
||||
'click_mana_1', 'click_mana_3', 'damage_5', 'damage_10', 'damage_pct_10',
|
||||
'crit_5', 'attack_speed_10', 'meditate_10', 'study_10', 'insight_5',
|
||||
'spell_echo_10', 'guardian_dmg_10', 'overpower_80'
|
||||
];
|
||||
const currentEffects = prev.unlockedEffects || [];
|
||||
allEffectIds.forEach(id => {
|
||||
if (!currentEffects.includes(id)) {
|
||||
currentEffects.push(id);
|
||||
}
|
||||
});
|
||||
return { ...prev, unlockedEffects: currentEffects };
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
|
||||
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getTotalAttunementRegen, getAvailableSkillCategories, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL, getAttunementConversionRate } from '@/lib/game/data/attunements';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import type { GameStore, AttunementState } from '@/lib/game/types';
|
||||
import type { AttunementState } from '@/lib/game/types';
|
||||
import { usePrestigeStore, useManaStore } from '@/lib/game/stores';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Lock, TrendingUp } from 'lucide-react';
|
||||
|
||||
export interface AttunementsTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
const attunements = store.attunements || {};
|
||||
export function AttunementsTab() {
|
||||
const attunements = usePrestigeStore((s) => s.attunements) || {};
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
|
||||
// Get active attunements
|
||||
const activeAttunements = Object.entries(attunements)
|
||||
@@ -66,8 +64,8 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
const primaryElem = def.primaryManaType ? ELEMENTS[def.primaryManaType] : null;
|
||||
|
||||
// Get current mana for this attunement's type
|
||||
const currentMana = def.primaryManaType ? store.elements[def.primaryManaType]?.current || 0 : 0;
|
||||
const maxMana = def.primaryManaType ? store.elements[def.primaryManaType]?.max || 50 : 50;
|
||||
const currentMana = def.primaryManaType ? elements[def.primaryManaType]?.current || 0 : 0;
|
||||
const maxMana = def.primaryManaType ? elements[def.primaryManaType]?.max || 50 : 50;
|
||||
|
||||
// Calculate level-scaled stats
|
||||
const levelMult = Math.pow(1.5, level - 1);
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getTotalAttunementRegen, getAvailableSkillCategories, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL, getAttunementConversionRate } from '@/lib/game/data/attunements';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import type { GameStore, AttunementState } from '@/lib/game/types';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Lock, TrendingUp } from 'lucide-react';
|
||||
|
||||
export interface AttunementsTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
const attunements = store.attunements || {};
|
||||
|
||||
// Get active attunements
|
||||
const activeAttunements = Object.entries(attunements)
|
||||
.filter(([, state]) => state.active)
|
||||
.map(([id]) => ATTUNEMENTS_DEF[id])
|
||||
.filter(Boolean);
|
||||
|
||||
// Calculate total regen from attunements
|
||||
const totalAttunementRegen = getTotalAttunementRegen(attunements);
|
||||
|
||||
// Get available skill categories
|
||||
const availableCategories = getAvailableSkillCategories(attunements);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Overview Card */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 text-sm">Your Attunements</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-gray-400 mb-3">
|
||||
Attunements are magical bonds tied to specific body locations. Each attunement grants unique capabilities,
|
||||
mana regeneration, and access to specialized skills. Level them up to increase their power.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge className="bg-teal-900/50 text-teal-300">
|
||||
+{totalAttunementRegen.toFixed(1)} raw mana/hr
|
||||
</Badge>
|
||||
<Badge className="bg-purple-900/50 text-purple-300">
|
||||
{activeAttunements.length} active attunement{activeAttunements.length !== 1 ? 's' : ''}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Attunement Slots */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Object.entries(ATTUNEMENTS_DEF).map(([id, def]) => {
|
||||
const state = attunements[id];
|
||||
const isActive = state?.active;
|
||||
const isUnlocked = state?.active || def.unlocked;
|
||||
const level = state?.level || 1;
|
||||
const xp = state?.experience || 0;
|
||||
const xpNeeded = getAttunementXPForLevel(level + 1);
|
||||
const xpProgress = xpNeeded > 0 ? (xp / xpNeeded) * 100 : 100;
|
||||
const isMaxLevel = level >= MAX_ATTUNEMENT_LEVEL;
|
||||
|
||||
// Get primary mana element info
|
||||
const primaryElem = def.primaryManaType ? ELEMENTS[def.primaryManaType] : null;
|
||||
|
||||
// Get current mana for this attunement's type
|
||||
const currentMana = def.primaryManaType ? store.elements[def.primaryManaType]?.current || 0 : 0;
|
||||
const maxMana = def.primaryManaType ? store.elements[def.primaryManaType]?.max || 50 : 50;
|
||||
|
||||
// Calculate level-scaled stats
|
||||
const levelMult = Math.pow(1.5, level - 1);
|
||||
const scaledRegen = def.rawManaRegen * levelMult;
|
||||
const scaledConversion = getAttunementConversionRate(id, level);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={id}
|
||||
className={`bg-gray-900/80 transition-all ${
|
||||
isActive
|
||||
? 'border-2 shadow-lg'
|
||||
: isUnlocked
|
||||
? 'border-gray-600'
|
||||
: 'border-gray-800 opacity-70'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: isActive ? def.color : undefined,
|
||||
boxShadow: isActive ? `0 0 20px ${def.color}30` : undefined
|
||||
}}
|
||||
>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-2xl">{def.icon}</span>
|
||||
<div>
|
||||
<CardTitle className="text-sm" style={{ color: isActive ? def.color : '#9CA3AF' }}>
|
||||
{def.name}
|
||||
</CardTitle>
|
||||
<div className="text-xs text-gray-500">
|
||||
{ATTUNEMENT_SLOT_NAMES[def.slot]}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isUnlocked && (
|
||||
<Lock className="w-4 h-4 text-gray-600" />
|
||||
)}
|
||||
{isActive && (
|
||||
<Badge className="text-xs" style={{ backgroundColor: `${def.color}30`, color: def.color }}>
|
||||
Lv.{level}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-xs text-gray-400">{def.desc}</p>
|
||||
|
||||
{/* Mana Type */}
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Primary Mana</span>
|
||||
{primaryElem ? (
|
||||
<span style={{ color: primaryElem.color }}>
|
||||
{primaryElem.sym} {primaryElem.name}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-purple-400">From Pacts</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mana bar (only for attunements with primary type) */}
|
||||
{primaryElem && isActive && (
|
||||
<div className="space-y-1">
|
||||
<Progress
|
||||
value={(currentMana / maxMana) * 100}
|
||||
className="h-2 bg-gray-800"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>{currentMana.toFixed(1)}</span>
|
||||
<span>/{maxMana}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats with level scaling */}
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="p-2 bg-gray-800/50 rounded">
|
||||
<div className="text-gray-500">Raw Regen</div>
|
||||
<div className="text-green-400 font-semibold">
|
||||
+{scaledRegen.toFixed(2)}/hr
|
||||
{level > 1 && <span className="text-xs ml-1">({((levelMult - 1) * 100).toFixed(0)}% bonus)</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2 bg-gray-800/50 rounded">
|
||||
<div className="text-gray-500">Conversion</div>
|
||||
<div className="text-cyan-400 font-semibold">
|
||||
{scaledConversion > 0 ? `${scaledConversion.toFixed(2)}/hr` : '—'}
|
||||
{level > 1 && scaledConversion > 0 && <span className="text-xs ml-1">({((levelMult - 1) * 100).toFixed(0)}% bonus)</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* XP Progress Bar */}
|
||||
{isUnlocked && state && !isMaxLevel && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500 flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3" />
|
||||
XP Progress
|
||||
</span>
|
||||
<span className="text-amber-400">{xp} / {xpNeeded}</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={xpProgress}
|
||||
className="h-2 bg-gray-800"
|
||||
/>
|
||||
<div className="text-xs text-gray-500">
|
||||
{isMaxLevel ? 'Max Level' : `${xpNeeded - xp} XP to Level ${level + 1}`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Max Level Indicator */}
|
||||
{isMaxLevel && (
|
||||
<div className="text-xs text-amber-400 text-center font-semibold">
|
||||
✨ MAX LEVEL ✨
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Capabilities */}
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-gray-500">Capabilities</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{def.capabilities.map(cap => (
|
||||
<Badge key={cap} variant="outline" className="text-xs">
|
||||
{cap === 'enchanting' && '✨ Enchanting'}
|
||||
{cap === 'disenchanting' && '🔄 Disenchant'} // TODO: Remove after bug 13 complete
|
||||
{cap === 'pacts' && '🤝 Pacts'}
|
||||
{cap === 'guardianPowers' && '💜 Guardian Powers'}
|
||||
{cap === 'elementalMastery' && '🌟 Elem. Mastery'}
|
||||
{cap === 'golemCrafting' && '🗿 Golems'}
|
||||
{cap === 'gearCrafting' && '⚒️ Gear'}
|
||||
{cap === 'earthShaping' && '⛰️ Earth Shaping'}
|
||||
{!['enchanting', 'pacts', 'guardianPowers',
|
||||
'elementalMastery', 'golemCrafting', 'gearCrafting', 'earthShaping'].includes(cap) && cap}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Unlock condition for locked attunements */}
|
||||
{!isUnlocked && def.unlockCondition && (
|
||||
<div className="text-xs text-amber-400 italic">
|
||||
🔒 {def.unlockCondition}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Available Skills Summary */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 text-sm">Available Skill Categories</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-gray-400 mb-2">
|
||||
Your attunements grant access to specialized skill categories:
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{availableCategories.map(cat => {
|
||||
const attunement = Object.values(ATTUNEMENTS_DEF).find(a =>
|
||||
a.skillCategories.includes(cat) && attunements[a.id]?.active
|
||||
);
|
||||
return (
|
||||
<Badge
|
||||
key={cat}
|
||||
className={attunement ? '' : 'bg-gray-700/50 text-gray-400'}
|
||||
style={attunement ? {
|
||||
backgroundColor: `${attunement.color}30`,
|
||||
color: attunement.color
|
||||
} : undefined}
|
||||
>
|
||||
{cat === 'mana' && '💧 Mana'}
|
||||
{cat === 'study' && '📚 Study'}
|
||||
{cat === 'research' && '🔮 Research'} // TODO: Remove after Bug 12 - research moved to mana
|
||||
{cat === 'ascension' && '⭐ Ascension'}
|
||||
{cat === 'enchant' && '✨ Enchanting'}
|
||||
{cat === 'effectResearch' && '🔬 Effect Research'}
|
||||
{cat === 'invocation' && '💜 Invocation'}
|
||||
{cat === 'pact' && '🤝 Pact Mastery'}
|
||||
{cat === 'fabrication' && '⚒️ Fabrication'}
|
||||
{cat === 'golemancy' && '🗿 Golemancy'}
|
||||
{!['mana', 'study', 'research', 'ascension', 'enchant', 'effectResearch',
|
||||
'invocation', 'pact', 'fabrication', 'golemancy'].includes(cat) && cat}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AttunementsTab.displayName = "AttunementsTab";
|
||||
@@ -227,13 +227,8 @@ export function EquipmentTab() {
|
||||
}
|
||||
};
|
||||
|
||||
// Get unified effects for equipment stats - move hook before conditional
|
||||
const equipmentInstancesForEffects = useCombatStore((s) => s.equipmentInstances);
|
||||
const equippedInstancesForEffects = useCombatStore((s) => s.equippedInstances);
|
||||
|
||||
const unifiedEffects = equipmentInstancesForEffects && equippedInstancesForEffects
|
||||
? getUnifiedEffects({ equipmentInstances, equippedInstances })
|
||||
: null;
|
||||
// Use already-fetched values for unified effects
|
||||
const unifiedEffects = getUnifiedEffects({ equipmentInstances, equippedInstances });
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-full overflow-x-hidden">
|
||||
|
||||
Reference in New Issue
Block a user