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

This commit is contained in:
Refactoring Agent
2026-05-05 12:45:07 +02:00
parent dc1aad3700
commit 221d3e4b41
20 changed files with 399 additions and 754 deletions
-2
View File
@@ -106,7 +106,6 @@ Mana-Loop/
│ │ │ │ ├── AchievementsTab.tsx
│ │ │ │ ├── ActivityLog.tsx
│ │ │ │ ├── AttunementsTab.tsx
│ │ │ │ ├── AttunementsTab.tsx.backup
│ │ │ │ ├── CategorySkillsList.tsx
│ │ │ │ ├── CombatStatsPanel.tsx
│ │ │ │ ├── CraftingTab.tsx
@@ -137,7 +136,6 @@ Mana-Loop/
│ │ │ │ └── index.ts
│ │ │ ├── AchievementsDisplay.tsx
│ │ │ ├── ActionButtons.tsx
│ │ │ ├── ActionButtons.tsx.backup
│ │ │ ├── CalendarDisplay.tsx
│ │ │ ├── ConfirmDialog.tsx
│ │ │ ├── CraftingProgress.tsx
+1
View File
@@ -73,6 +73,7 @@ function GrimoireTab() {
// Only access SPELLS_DEF on client-side
if (typeof window !== 'undefined' && SPELLS_DEF) {
const filtered = Object.values(SPELLS_DEF || {}).filter((s: any) => s.grimoire);
// eslint-disable-next-line react-hooks/set-state-in-effect
setGrimoireSpells(filtered);
}
}, []);
@@ -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";
+20 -15
View File
@@ -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>
+26 -17
View File
@@ -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,10 +16,15 @@ 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 skillInfo = getSkillDisplayInfo(skillState, skillId);
const {
currentTier,
tieredSkillId,
@@ -27,13 +32,14 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
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'));
@@ -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">
@@ -121,7 +128,7 @@ export function SkillRow({ skillId, onUpgradeClick }: SkillRowProps) {
{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";
+16 -16
View File
@@ -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;
}
+12 -13
View File
@@ -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,17 +6,22 @@ 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;
};
@@ -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,14 +4,20 @@ 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() {
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);
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);
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">
@@ -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>
+11 -9
View File
@@ -4,20 +4,22 @@ 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);
}
};
@@ -31,9 +33,9 @@ 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">
+9 -6
View File
@@ -3,18 +3,21 @@
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);
}
};
@@ -28,7 +31,7 @@ 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
+42 -40
View File
@@ -10,18 +10,32 @@ 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);
@@ -30,18 +44,16 @@ export function GameStateDebug() {
};
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 (
@@ -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,19 +182,19 @@ 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>
@@ -191,9 +203,9 @@ export function GameStateDebug() {
<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>
+136 -109
View File
@@ -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 };
});
}}
>
+7 -9
View File
@@ -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";
+2 -7
View File
@@ -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">