Completely remove legacy skill system and tests
This commit is contained in:
@@ -1,57 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
import { SKILL_CATEGORIES } from '@/lib/game/constants';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { SkillUpgradeDialog } from './SkillsTab/SkillUpgradeDialog';
|
||||
import { SkillStudyProgress } from './SkillsTab/SkillStudyProgress';
|
||||
import { SkillCategory } from './SkillsTab/SkillCategory';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
|
||||
export function SkillsTab() {
|
||||
const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
|
||||
const [upgradeDialogSkill, setUpgradeDialogSkill] = useState<string | null>(null);
|
||||
const [upgradeDialogMilestone, setUpgradeDialogMilestone] = useState<5 | 10>(5);
|
||||
|
||||
const handleUpgradeClick = (skillId: string, milestone: 5 | 10) => {
|
||||
setUpgradeDialogSkill(skillId);
|
||||
setUpgradeDialogMilestone(milestone);
|
||||
};
|
||||
|
||||
const handleUpgradeClose = () => {
|
||||
setUpgradeDialogSkill(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<DebugName name="SkillsTab">
|
||||
<div className="space-y-4">
|
||||
{/* Upgrade Selection Dialog */}
|
||||
<SkillUpgradeDialog
|
||||
skillId={upgradeDialogSkill}
|
||||
milestone={upgradeDialogMilestone}
|
||||
onClose={handleUpgradeClose}
|
||||
/>
|
||||
|
||||
{/* Current Study Progress */}
|
||||
{currentStudyTarget && currentStudyTarget.type === 'skill' && (
|
||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
||||
<CardContent className="pt-4">
|
||||
<SkillStudyProgress />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{SKILL_CATEGORIES.map((cat) => (
|
||||
<SkillCategory
|
||||
key={cat.id}
|
||||
categoryId={cat.id}
|
||||
onUpgradeClick={handleUpgradeClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</DebugName>
|
||||
);
|
||||
}
|
||||
|
||||
SkillsTab.displayName = "SkillsTab";
|
||||
@@ -1,265 +0,0 @@
|
||||
'use client';
|
||||
|
||||
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 { useSkillStore } from '@/lib/game/stores';
|
||||
import { useManaStore } from '@/lib/game/stores';
|
||||
|
||||
export function SkillDebug() {
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
|
||||
const incrementSkillLevel = (skillId: string) => {
|
||||
useSkillStore.getState().incrementSkillLevel(skillId);
|
||||
};
|
||||
|
||||
const setSkillLevel = (skillId: string, level: number) => {
|
||||
useSkillStore.getState().setSkillLevel(skillId, level);
|
||||
};
|
||||
|
||||
const unlockAllEffects = () => {
|
||||
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'
|
||||
];
|
||||
useManaStore.setState((prev: any) => {
|
||||
const currentEffects = prev.unlockedEffects || [];
|
||||
const newEffects = [...currentEffects];
|
||||
effectIds.forEach(id => {
|
||||
if (!newEffects.includes(id)) {
|
||||
newEffects.push(id);
|
||||
}
|
||||
});
|
||||
return { ...prev, unlockedEffects: newEffects };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-purple-400 text-sm flex items-center gap-2">
|
||||
<BookOpen className="w-4 h-4" />
|
||||
Skill Research Debug
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{/* Enchanting Skills */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-2">Enchanting Skills:</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Level up all enchanting skills by 1
|
||||
const enchantSkills = ['enchanting', 'efficientEnchant', 'enchantSpeed', 'essenceRefining'];
|
||||
enchantSkills.forEach(skillId => {
|
||||
const currentLevel = skills[skillId] || 0;
|
||||
if (currentLevel < 10) {
|
||||
incrementSkillLevel(skillId);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
+1 All Enchanting
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Max all enchanting skills
|
||||
const enchantSkills = ['enchanting', 'efficientEnchant', 'enchantSpeed', 'essenceRefining'];
|
||||
enchantSkills.forEach(skillId => {
|
||||
setSkillLevel(skillId, 10);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Max All Enchanting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mana Skills */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-2">Mana Skills:</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const manaSkills = ['manaWell', 'manaFlow', 'manaOverflow', 'fireManaCap', 'waterManaCap', 'airManaCap', 'earthManaCap', 'lightManaCap', 'darkManaCap', 'deathManaCap', 'metalManaCap', 'sandManaCap', 'lightningManaCap', 'transferenceManaCap'];
|
||||
manaSkills.forEach(skillId => {
|
||||
const currentLevel = skills[skillId] || 0;
|
||||
if (currentLevel < 10) {
|
||||
incrementSkillLevel(skillId);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
+1 All Mana
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const manaSkills = ['manaWell', 'manaFlow', 'manaOverflow', 'fireManaCap', 'waterManaCap', 'airManaCap', 'earthManaCap', 'lightManaCap', 'darkManaCap', 'deathManaCap', 'metalManaCap', 'sandManaCap', 'lightningManaCap', 'transferenceManaCap'];
|
||||
manaSkills.forEach(skillId => {
|
||||
setSkillLevel(skillId, 10);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Max All Mana
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Study Skills */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-2">Study Skills:</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const studySkills = ['quickLearner', 'focusedMind', 'meditation', 'knowledgeRetention'];
|
||||
studySkills.forEach(skillId => {
|
||||
const currentLevel = skills[skillId] || 0;
|
||||
if (currentLevel < 10) {
|
||||
incrementSkillLevel(skillId);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
+1 All Study
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const studySkills = ['quickLearner', 'focusedMind', 'meditation', 'knowledgeRetention'];
|
||||
studySkills.forEach(skillId => {
|
||||
setSkillLevel(skillId, 10);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Max All Study
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Crafting Skills */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-2">Crafting Skills:</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const craftSkills = ['effCrafting', 'fieldRepair', 'elemCrafting'];
|
||||
craftSkills.forEach(skillId => {
|
||||
const currentLevel = skills[skillId] || 0;
|
||||
if (currentLevel < 10) {
|
||||
incrementSkillLevel(skillId);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
+1 All Crafting
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const craftSkills = ['effCrafting', 'fieldRepair', 'elemCrafting'];
|
||||
craftSkills.forEach(skillId => {
|
||||
setSkillLevel(skillId, 10);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Max All Crafting
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Research Effects */}
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-2">Research Effects:</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
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 => {
|
||||
setSkillLevel(skillId, 1);
|
||||
});
|
||||
}}
|
||||
>
|
||||
Unlock All Research
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
unlockAllEffects();
|
||||
}}
|
||||
>
|
||||
Unlock All Effects
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Max All */}
|
||||
<Separator className="bg-gray-700" />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={() => {
|
||||
// Max all skills to level 10
|
||||
Object.keys(skills).forEach(skillId => {
|
||||
setSkillLevel(skillId, 10);
|
||||
});
|
||||
// Unlock all effects
|
||||
unlockAllEffects();
|
||||
}}
|
||||
>
|
||||
🚀 Max Everything
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
SkillDebug.displayName = "SkillDebug";
|
||||
@@ -1,35 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
||||
import { useCombatStore, useManaStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
|
||||
export function AchievementsTab() {
|
||||
const achievements = useCombatStore((s) => s.achievements);
|
||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
||||
const totalManaGathered = useManaStore((s) => s.totalManaGathered);
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const totalSpellsCast = useCombatStore((s) => s.totalSpellsCast);
|
||||
const totalDamageDealt = useCombatStore((s) => s.totalDamageDealt);
|
||||
const totalCraftsCompleted = useCombatStore((s) => s.totalCraftsCompleted);
|
||||
|
||||
return (
|
||||
<DebugName name="AchievementsTab">
|
||||
<div className="space-y-4">
|
||||
<AchievementsDisplay
|
||||
achievements={achievements}
|
||||
gameState={{
|
||||
maxFloorReached,
|
||||
totalManaGathered,
|
||||
signedPacts,
|
||||
totalSpellsCast,
|
||||
totalDamageDealt,
|
||||
totalCraftsCompleted,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DebugName>
|
||||
);
|
||||
}
|
||||
|
||||
AchievementsTab.displayName = "AchievementsTab";
|
||||
@@ -1,69 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
||||
|
||||
interface ActivityLogProps {
|
||||
activityLog?: ActivityLogEntry[];
|
||||
maxEntries?: number;
|
||||
}
|
||||
|
||||
export function ActivityLog({ activityLog, maxEntries = 50 }: ActivityLogProps) {
|
||||
const entries = activityLog || [];
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScrollArea className="h-48">
|
||||
<div className="space-y-1">
|
||||
{entries.slice(0, maxEntries).map((entry, i) => {
|
||||
const isLatest = i === 0;
|
||||
const color = getEventStyle(entry.eventType);
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={`text-xs ${isLatest ? 'text-gray-200 font-semibold' : color}`}
|
||||
>
|
||||
{entry.message}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{entries.length === 0 && (
|
||||
<div className="text-xs text-gray-500 italic">No activity yet...</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function getEventStyle(eventType: string): string {
|
||||
switch (eventType) {
|
||||
case 'enemy_defeated':
|
||||
case 'floor_cleared':
|
||||
return 'text-green-400';
|
||||
case 'damage_dealt':
|
||||
return 'text-red-400';
|
||||
case 'dodge':
|
||||
return 'text-yellow-400';
|
||||
case 'armor_proc':
|
||||
return 'text-blue-400';
|
||||
case 'special_effect':
|
||||
return 'text-purple-400';
|
||||
case 'floor_transition':
|
||||
return 'text-cyan-400';
|
||||
case 'spell_cast':
|
||||
return 'text-amber-400';
|
||||
case 'golem_attack':
|
||||
return 'text-orange-400';
|
||||
case 'puzzle_solved':
|
||||
return 'text-pink-400';
|
||||
default:
|
||||
return 'text-gray-300';
|
||||
}
|
||||
}
|
||||
@@ -1,270 +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 { 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';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
|
||||
export function AttunementsTab() {
|
||||
const attunements = usePrestigeStore((s) => s.attunements) || {};
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
|
||||
// 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 (
|
||||
<DebugName name="AttunementsTab">
|
||||
<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 ? 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);
|
||||
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>
|
||||
</DebugName>
|
||||
);
|
||||
}
|
||||
|
||||
AttunementsTab.displayName = "AttunementsTab";
|
||||
@@ -1,120 +0,0 @@
|
||||
// CategorySkillsList - Displays skills for a specific category
|
||||
// Migrated to use hooks directly (removed GameStore prop)
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { useSkillStore, useGameStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
import { SkillRow } from './SkillRow';
|
||||
import type { GameStore } from '@/lib/game/stores'; // Keep type import for backward compatibility
|
||||
|
||||
interface CategorySkillsListProps {
|
||||
categoryId: string;
|
||||
categoryName: string;
|
||||
skills: Record<string, number>;
|
||||
skillUpgrades: Record<string, string[]>;
|
||||
skillTiers: Record<string, number>;
|
||||
prestigeUpgrades: Record<string, number>;
|
||||
studySpeedMult: number;
|
||||
upgradeEffects: any;
|
||||
currentStudyTarget: any;
|
||||
onStartStudying: (skillId: string) => void;
|
||||
onParallelStudy: (skillId: string) => void;
|
||||
onCancelStudy: () => void;
|
||||
onOpenUpgradeDialog: (skillId: string, milestone: 5 | 10) => void;
|
||||
onTierUp: (skillId: string) => void;
|
||||
pendingSelections: string[];
|
||||
setPendingSelections: (selections: string[]) => void;
|
||||
}
|
||||
|
||||
export function CategorySkillsList({
|
||||
categoryId,
|
||||
categoryName,
|
||||
skills,
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
prestigeUpgrades,
|
||||
studySpeedMult,
|
||||
upgradeEffects,
|
||||
currentStudyTarget,
|
||||
onStartStudying,
|
||||
onParallelStudy,
|
||||
onCancelStudy,
|
||||
onOpenUpgradeDialog,
|
||||
onTierUp,
|
||||
pendingSelections,
|
||||
setPendingSelections,
|
||||
}: CategorySkillsListProps) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
const categorySkills = Object.entries(SKILLS_DEF || {})
|
||||
.filter(([, def]) => def.category === categoryId)
|
||||
.sort((a, b) => (a[1].tier || 0) - (b[1].tier || 0));
|
||||
|
||||
const toggleCollapse = () => setCollapsed(!collapsed);
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div
|
||||
className="flex items-center gap-2 mb-2 cursor-pointer"
|
||||
onClick={toggleCollapse}
|
||||
>
|
||||
<span className="text-sm font-semibold text-gray-300">
|
||||
{categoryName} ({categorySkills.length})
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{collapsed ? '▼' : '▶'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{!collapsed && (
|
||||
<div className="space-y-2">
|
||||
{categorySkills.map(([skillId, def]) => {
|
||||
const skillLevel = skills[skillId] || 0;
|
||||
const tier = skillTiers[skillId] || 0;
|
||||
const tierMult = getTierMultiplier(skillId)(tier);
|
||||
const isStudying = currentStudyTarget?.id === skillId;
|
||||
const isParallel = currentStudyTarget?.type === 'parallel' && currentStudyTarget?.id === skillId;
|
||||
|
||||
// Get upgrade choices for this skill
|
||||
const store = useGameStore.getState();
|
||||
const { available, selected } = store.getSkillUpgradeChoices(skillId, tier as 5 | 10);
|
||||
|
||||
return (
|
||||
<SkillRow
|
||||
key={skillId}
|
||||
skillId={skillId}
|
||||
skillDef={def}
|
||||
skillLevel={skillLevel}
|
||||
tier={tier}
|
||||
tierMult={tierMult}
|
||||
isStudying={isStudying}
|
||||
isParallel={isParallel}
|
||||
studySpeedMult={studySpeedMult}
|
||||
upgradeEffects={upgradeEffects}
|
||||
availableUpgrades={available}
|
||||
selectedUpgrades={selected}
|
||||
pendingSelections={pendingSelections}
|
||||
onToggleUpgrade={(upgradeId) => {
|
||||
if (pendingSelections.includes(upgradeId)) {
|
||||
setPendingSelections(pendingSelections.filter(id => id !== upgradeId));
|
||||
} else {
|
||||
setPendingSelections([...pendingSelections, upgradeId]);
|
||||
}
|
||||
}}
|
||||
onStartStudying={() => onStartStudying(skillId)}
|
||||
onParallelStudy={() => onParallelStudy(skillId)}
|
||||
onTierUp={() => onTierUp(skillId)}
|
||||
onOpenUpgradeDialog={(milestone) => onOpenUpgradeDialog(skillId, milestone)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||
import { Zap, Shield, ShieldCheck, Wind, Heart, Mountain, BookOpen } from 'lucide-react';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
||||
import type { CombatStatsPanelProps } from '@/lib/game/types';
|
||||
import { useCombatStore } from '@/lib/game/stores';
|
||||
import { useSkillStore } from '@/lib/game/stores';
|
||||
import { usePrestigeStore } from '@/lib/game/stores';
|
||||
|
||||
export function CombatStatsPanel({
|
||||
activeEquipmentSpells,
|
||||
totalDPS,
|
||||
calcDamage,
|
||||
formatSpellCost,
|
||||
getSpellCostColor,
|
||||
SPELLS_DEF,
|
||||
upgradeEffects,
|
||||
canCastSpell,
|
||||
studySpeedMult,
|
||||
storeCurrentAction,
|
||||
}: CombatStatsPanelProps) {
|
||||
const golemancy = useCombatStore((s) => s.golemancy);
|
||||
const equipmentSpellStates = useCombatStore((s) => s.equipmentSpellStates);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const activeGolems = golemancy.summonedGolems;
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Combat Stats</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-sm text-gray-400">
|
||||
Total DPS: <span className="text-amber-400 font-semibold">{storeCurrentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}</span>
|
||||
</div>
|
||||
|
||||
{activeEquipmentSpells.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-gray-500 font-semibold uppercase tracking-wider">Active Spells</div>
|
||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) return null;
|
||||
const spellState = equipmentSpellStates?.find(
|
||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
||||
);
|
||||
const progress = spellState?.castProgress || 0;
|
||||
const canCast = canCastSpell(spellId);
|
||||
|
||||
return (
|
||||
<div key={`${spellId}-${equipmentId}`} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
||||
{spellDef.name}
|
||||
{spellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200 text-xs">Basic</Badge>}
|
||||
{spellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100 text-xs">Legendary</Badge>}
|
||||
</span>
|
||||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{canCast ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mb-1">
|
||||
⚔️ {fmt(calcDamage({ skills, signedPacts }, spellId))} dmg • {' '}
|
||||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
||||
{formatSpellCost(spellDef.cost)}
|
||||
</span>
|
||||
{' '}• ⚡ {fmt(Math.floor(calcDamage({ skills, signedPacts }, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||||
</div>
|
||||
{storeCurrentAction === 'climb' && (
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>Cast</span>
|
||||
<span>{(progress * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, progress * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color}99, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeGolems.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500 font-semibold uppercase tracking-wider">
|
||||
<Mountain className="w-3 h-3" />
|
||||
Active Golems
|
||||
</div>
|
||||
{activeGolems.map((summoned) => {
|
||||
const golemDef = GOLEMS_DEF[summoned.golemId];
|
||||
if (!golemDef) return null;
|
||||
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
||||
const damage = getGolemDamage(summoned.golemId, skills);
|
||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, skills);
|
||||
|
||||
return (
|
||||
<div key={summoned.golemId} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mountain className="w-3 h-3" style={{ color: elemColor }} />
|
||||
<span className="text-xs font-semibold" style={{ color: elemColor }}>
|
||||
{golemDef.name}
|
||||
</span>
|
||||
</div>
|
||||
{golemDef.isAoe && (
|
||||
<Badge variant="outline" className="text-xs py-0">AOE {golemDef.aoeTargets}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">
|
||||
⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr
|
||||
</div>
|
||||
{storeCurrentAction === 'climb' && summoned.attackProgress > 0 && (
|
||||
<div className="space-y-0.5 mt-1">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>Attack</span>
|
||||
<span>{Math.min(100, (summoned.attackProgress * 100)).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, summoned.attackProgress * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${elemColor}99, ${elemColor})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 border-t border-gray-700">
|
||||
<div className="text-xs text-gray-500 mb-1">Study Speed: {Math.round(studySpeedMult * 100)}%</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function fmt(value: number): string {
|
||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||||
return value.toFixed(0);
|
||||
}
|
||||
|
||||
function fmtDec(value: number): string {
|
||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||||
return value.toFixed(0);
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { GameCard } from '@/components/ui/game-card';
|
||||
import { SectionHeader } from '@/components/ui/section-header';
|
||||
import { ActionButton } from '@/components/ui/action-button';
|
||||
import { Scroll, Hammer, Sparkles, Anvil } from 'lucide-react';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import {
|
||||
EnchantmentDesigner,
|
||||
EnchantmentPreparer,
|
||||
EnchantmentApplier,
|
||||
EquipmentCrafter,
|
||||
} from '@/components/game/crafting';
|
||||
import { useCombatStore, useCraftingStore } from '@/lib/game/stores';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
import { useGameToast } from '@/components/game/GameToast';
|
||||
import type { DesignEffect } from '@/lib/game/types';
|
||||
|
||||
export function CraftingTab() {
|
||||
const showToast = useGameToast();
|
||||
const currentAction = useCombatStore((s) => s.currentAction);
|
||||
const designProgress = useCraftingStore((s) => s.designProgress);
|
||||
const preparationProgress = useCraftingStore((s) => s.preparationProgress);
|
||||
const applicationProgress = useCraftingStore((s) => s.applicationProgress);
|
||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
||||
const pauseApplication = useCraftingStore((s) => s.pauseApplication);
|
||||
const resumeApplication = useCraftingStore((s) => s.resumeApplication);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'fabricate' | 'enchant'>('fabricate');
|
||||
const [enchantStage, setEnchantStage] = useState<'design' | 'prepare' | 'apply'>('design');
|
||||
|
||||
// Enchant state
|
||||
const [selectedEquipmentType, setSelectedEquipmentType] = useState<string | null>(null);
|
||||
const [selectedEffects, setSelectedEffects] = useState<DesignEffect[]>([]);
|
||||
const [designName, setDesignName] = useState('');
|
||||
const [selectedDesign, setSelectedDesign] = useState<string | null>(null);
|
||||
const [selectedEquipmentInstance, setSelectedEquipmentInstance] = useState<string | null>(null);
|
||||
|
||||
// Safe toFixed helper
|
||||
const safeToFixed = (value: number | undefined, decimals: number = 0): string => {
|
||||
if (value === undefined || isNaN(value)) return '0';
|
||||
return value.toFixed(decimals);
|
||||
};
|
||||
|
||||
// Safe percentage calculation
|
||||
const calcPercent = (progress: number, required: number): number => {
|
||||
if (!required || required === 0) return 0;
|
||||
return (progress / required) * 100;
|
||||
};
|
||||
|
||||
// Handle enchantment application with toast
|
||||
const handleEnchantmentApplied = () => {
|
||||
showToast('success', 'Enchantment Applied', 'The enchantment has been successfully applied!');
|
||||
};
|
||||
|
||||
// Handle enchantment capacity exceeded
|
||||
const handleCapacityExceeded = (itemName: string, used: number, total: number) => {
|
||||
showToast('error', 'Enchantment Capacity Exceeded', `${itemName} can only hold ${total} enchantments (${used}/${total} used). Remove some enchantments first.`);
|
||||
};
|
||||
|
||||
return (
|
||||
<DebugName name="CraftingTab">
|
||||
<div className="space-y-4 max-w-full overflow-x-hidden">
|
||||
{/* Top Sub-Tabs: Fabricate / Enchant */}
|
||||
<GameCard variant="default" className="p-4">
|
||||
<div className="flex justify-center gap-2">
|
||||
<ActionButton
|
||||
variant={activeTab === 'fabricate' ? 'primary' : 'secondary'}
|
||||
onClick={() => setActiveTab('fabricate')}
|
||||
className={activeTab === 'fabricate' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||
>
|
||||
<Anvil size={14} className="mr-1" />
|
||||
Fabricate
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
variant={activeTab === 'enchant' ? 'primary' : 'secondary'}
|
||||
onClick={() => setActiveTab('enchant')}
|
||||
className={activeTab === 'enchant' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||
>
|
||||
<Sparkles size={14} className="mr-1" />
|
||||
Enchant
|
||||
</ActionButton>
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
{/* Fabricate Content: EquipmentCrafter */}
|
||||
{activeTab === 'fabricate' && (
|
||||
<EquipmentCrafter />
|
||||
)}
|
||||
|
||||
{/* Enchant Content: Design → Prepare → Apply workflow */}
|
||||
{activeTab === 'enchant' && (
|
||||
<div className="space-y-4">
|
||||
{/* Enchant Sub-Navigation (no numbered stepper) */}
|
||||
<GameCard variant="default" className="p-4">
|
||||
<div className="flex justify-center gap-2 flex-wrap">
|
||||
<ActionButton
|
||||
variant={enchantStage === 'design' ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setEnchantStage('design')}
|
||||
className={enchantStage === 'design' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||
>
|
||||
<Scroll size={14} className="mr-1" />
|
||||
Design
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
variant={enchantStage === 'prepare' ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setEnchantStage('prepare')}
|
||||
className={enchantStage === 'prepare' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||
>
|
||||
<Hammer size={14} className="mr-1" />
|
||||
Prepare
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
variant={enchantStage === 'apply' ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
onClick={() => setEnchantStage('apply')}
|
||||
className={enchantStage === 'apply' ? 'ring-2 ring-[var(--interactive-primary)]' : ''}
|
||||
>
|
||||
<Sparkles size={14} className="mr-1" />
|
||||
Apply
|
||||
</ActionButton>
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
{/* Enchant Stage Content */}
|
||||
{enchantStage === 'design' && (
|
||||
<EnchantmentDesigner
|
||||
selectedEquipmentType={selectedEquipmentType}
|
||||
setSelectedEquipmentType={setSelectedEquipmentType}
|
||||
selectedEffects={selectedEffects}
|
||||
setSelectedEffects={setSelectedEffects}
|
||||
designName={designName}
|
||||
setDesignName={setDesignName}
|
||||
selectedDesign={selectedDesign}
|
||||
setSelectedDesign={setSelectedDesign}
|
||||
/>
|
||||
)}
|
||||
{enchantStage === 'prepare' && (
|
||||
<EnchantmentPreparer
|
||||
selectedEquipmentInstance={selectedEquipmentInstance}
|
||||
setSelectedEquipmentInstance={setSelectedEquipmentInstance}
|
||||
/>
|
||||
)}
|
||||
{enchantStage === 'apply' && (
|
||||
<EnchantmentApplier
|
||||
selectedEquipmentInstance={selectedEquipmentInstance}
|
||||
setSelectedEquipmentInstance={setSelectedEquipmentInstance}
|
||||
selectedDesign={selectedDesign}
|
||||
setSelectedDesign={setSelectedDesign}
|
||||
onEnchantmentApplied={handleEnchantmentApplied}
|
||||
onCapacityExceeded={handleCapacityExceeded}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Activity Indicator: Crafting */}
|
||||
{currentAction === 'craft' && equipmentCraftingProgress && (
|
||||
<GameCard variant="default" className="border-[var(--mana-water)]/60 bg-[var(--mana-water)]/10">
|
||||
<SectionHeader
|
||||
title="Crafting Equipment"
|
||||
action={
|
||||
<span className="text-sm text-[var(--text-muted)]">
|
||||
{safeToFixed(calcPercent(equipmentCraftingProgress.progress, equipmentCraftingProgress.required), 0)}%
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<Progress
|
||||
value={calcPercent(equipmentCraftingProgress.progress, equipmentCraftingProgress.required)}
|
||||
className="h-3 bg-[var(--bg-sunken)]"
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-2 text-sm text-[var(--text-secondary)]">
|
||||
<Anvil size={16} className="text-[var(--mana-water)]" />
|
||||
<span>Crafting equipment...</span>
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Current Activity Indicator: Designing */}
|
||||
{currentAction === 'design' && designProgress && (
|
||||
<GameCard variant="default" className="border-[var(--mana-stellar)]/60 bg-[var(--mana-stellar)]/10">
|
||||
<SectionHeader
|
||||
title="Designing Enchantment"
|
||||
action={
|
||||
<ActionButton variant="ghost" size="sm" onClick={() => useCraftingStore.getState().cancelDesign()}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
}
|
||||
/>
|
||||
<Progress
|
||||
value={calcPercent(designProgress.progress, designProgress.required)}
|
||||
className="h-3 bg-[var(--bg-sunken)]"
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-2 text-sm text-[var(--text-secondary)]">
|
||||
<Scroll size={16} className="text-[var(--mana-stellar)]" />
|
||||
<span>Designing: {designProgress.name}</span>
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Current Activity Indicator: Preparing */}
|
||||
{currentAction === 'prepare' && preparationProgress && (
|
||||
<GameCard variant="default" className="border-[var(--color-warning)]/60 bg-[var(--color-warning)]/10">
|
||||
<SectionHeader
|
||||
title="Preparing Equipment"
|
||||
action={
|
||||
<ActionButton variant="ghost" size="sm" onClick={() => useCraftingStore.getState().cancelPreparation()}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
}
|
||||
/>
|
||||
<Progress
|
||||
value={calcPercent(preparationProgress.progress, preparationProgress.required)}
|
||||
className="h-3 bg-[var(--bg-sunken)]"
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-2 text-sm text-[var(--text-secondary)]">
|
||||
<Hammer size={16} className="text-[var(--color-warning)]" />
|
||||
<span>Preparing equipment...</span>
|
||||
<span className="text-[var(--text-muted)] ml-auto">
|
||||
Mana paid: {fmt(preparationProgress.manaCostPaid)}
|
||||
</span>
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Current Activity Indicator: Enchanting */}
|
||||
{currentAction === 'enchant' && applicationProgress && (
|
||||
<GameCard variant="default" className="border-[var(--mana-light)]/60 bg-[var(--mana-light)]/10">
|
||||
<SectionHeader
|
||||
title={applicationProgress.paused ? "Enchantment Paused" : "Applying Enchantment"}
|
||||
action={
|
||||
<div className="flex gap-2">
|
||||
{applicationProgress.paused ? (
|
||||
<ActionButton size="sm" onClick={resumeApplication}>Resume</ActionButton>
|
||||
) : (
|
||||
<>
|
||||
<ActionButton variant="secondary" size="sm" onClick={pauseApplication}>Pause</ActionButton>
|
||||
<ActionButton variant="ghost" size="sm" onClick={() => {
|
||||
useCraftingStore.getState().cancelApplication();
|
||||
showToast('warning', 'Enchantment Cancelled', 'The enchantment application was cancelled.');
|
||||
}}>Cancel</ActionButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Progress
|
||||
value={calcPercent(applicationProgress.progress, applicationProgress.required)}
|
||||
className="h-3 bg-[var(--bg-sunken)]"
|
||||
/>
|
||||
<div className="flex items-center gap-2 mt-2 text-sm text-[var(--text-secondary)]">
|
||||
<Sparkles size={16} className="text-[var(--mana-light)]" />
|
||||
<span>{applicationProgress.paused ? 'Enchantment paused' : 'Applying enchantment...'}</span>
|
||||
<span className="text-[var(--text-muted)] ml-auto">
|
||||
{safeToFixed(calcPercent(applicationProgress.progress, applicationProgress.required), 0)}%
|
||||
</span>
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
</div>
|
||||
</DebugName>
|
||||
);
|
||||
}
|
||||
|
||||
CraftingTab.displayName = 'CraftingTab';
|
||||
@@ -1,32 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { GameStateDebug } from '@/components/game/debug/GameStateDebug';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
import {
|
||||
SkillDebug,
|
||||
AttunementDebug,
|
||||
ElementDebug,
|
||||
GolemDebug,
|
||||
PactDebug
|
||||
} from '@/components/game/debug';
|
||||
|
||||
export function DebugTab() {
|
||||
return (
|
||||
<DebugName name="DebugTab">
|
||||
<div className="space-y-4">
|
||||
<GameStateDebug />
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<AttunementDebug />
|
||||
<ElementDebug />
|
||||
</div>
|
||||
|
||||
<SkillDebug />
|
||||
<GolemDebug />
|
||||
<PactDebug />
|
||||
</div>
|
||||
</DebugName>
|
||||
);
|
||||
}
|
||||
|
||||
DebugTab.displayName = "DebugTab";
|
||||
@@ -1,48 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface EnchantmentsPanelProps {
|
||||
enchantments: Array<{ effectId: string; stacks: number }>;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function EnchantmentsPanel({
|
||||
enchantments,
|
||||
compact = false,
|
||||
}: EnchantmentsPanelProps) {
|
||||
if (enchantments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap gap-1 ${compact ? 'mt-1' : ''}`}>
|
||||
{enchantments.map((ench, i) => {
|
||||
const effect = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||
return (
|
||||
<TooltipProvider key={i}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs cursor-help border-[var(--border-default)] text-[var(--text-secondary)]"
|
||||
>
|
||||
{effect?.name || ench.effectId}
|
||||
{ench.stacks > 1 && ` x${ench.stacks}`}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
||||
<p>{effect?.description || 'Unknown effect'}</p>
|
||||
<p className="text-[var(--text-muted)] text-xs">
|
||||
Category: {effect?.category || 'unknown'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { EquipmentSlot } from '@/lib/game/data/equipment';
|
||||
// GameStore import removed - not used
|
||||
import { ActionButton } from '@/components/ui/action-button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface EquipmentControlsProps {
|
||||
onUnequip: (slot: EquipmentSlot) => void;
|
||||
onDelete: (instanceId: string, name: string) => void;
|
||||
selectedSlot: EquipmentSlot | null;
|
||||
isSlotBlocked: (slot: EquipmentSlot) => boolean;
|
||||
isEquipped: (instanceId: string) => boolean;
|
||||
getEquippableSlots: (typeId: string) => EquipmentSlot[];
|
||||
}
|
||||
|
||||
export function EquipmentControls({
|
||||
onUnequip,
|
||||
onDelete,
|
||||
selectedSlot,
|
||||
isSlotBlocked,
|
||||
isEquipped,
|
||||
getEquippableSlots,
|
||||
}: EquipmentControlsProps) {
|
||||
const SLOT_NAMES = {
|
||||
mainHand: 'Main Hand',
|
||||
offHand: 'Off Hand',
|
||||
head: 'Head',
|
||||
body: 'Body',
|
||||
hands: 'Hands',
|
||||
feet: 'Feet',
|
||||
accessory1: 'Accessory 1',
|
||||
accessory2: 'Accessory 2',
|
||||
} as const;
|
||||
|
||||
return {
|
||||
renderUnequipButton: (slot: EquipmentSlot, instanceName: string) => (
|
||||
<ActionButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUnequip(slot);
|
||||
}}
|
||||
aria-label={`Unequip ${instanceName}`}
|
||||
>
|
||||
<X size={14} />
|
||||
</ActionButton>
|
||||
),
|
||||
|
||||
renderDeleteButton: (instanceId: string, name: string) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<ActionButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20"
|
||||
onClick={() => onDelete(instanceId, name)}
|
||||
aria-label={`Delete ${name}`}
|
||||
>
|
||||
<X size={14} />
|
||||
</ActionButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
||||
<p>Delete this item</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { EquipmentSlot } from '@/lib/game/data/equipment';
|
||||
import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||
import type { EquipmentInstance } from '@/lib/game/types';
|
||||
import { GameCard } from '@/components/ui/game-card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { EnchantmentsPanel } from './EnchantmentsPanel';
|
||||
|
||||
interface EquipmentInventoryProps {
|
||||
unequippedItems: EquipmentInstance[];
|
||||
onEquip: (instanceId: string, slot: EquipmentSlot) => void;
|
||||
onDelete: (instanceId: string, name: string) => void;
|
||||
getEquippableSlots: (typeId: string) => EquipmentSlot[];
|
||||
SLOT_NAMES: Record<EquipmentSlot, string>;
|
||||
SLOT_ICONS: Record<EquipmentSlot, React.ElementType>;
|
||||
}
|
||||
|
||||
export function EquipmentInventory({
|
||||
unequippedItems,
|
||||
onEquip,
|
||||
onDelete,
|
||||
getEquippableSlots,
|
||||
SLOT_NAMES,
|
||||
SLOT_ICONS,
|
||||
}: EquipmentInventoryProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 max-h-96 overflow-y-auto">
|
||||
{unequippedItems.map((instance) => {
|
||||
const equipmentType = EQUIPMENT_TYPES[instance.typeId];
|
||||
const validSlots = getEquippableSlots(instance.typeId);
|
||||
|
||||
return (
|
||||
<GameCard
|
||||
key={instance.instanceId}
|
||||
variant="default"
|
||||
className={`${getRarityBorderColor(instance.rarity)} ${getRarityBgColor(instance.rarity)}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<div className={`font-semibold text-sm ${getRarityTextColor(instance.rarity)}`}>
|
||||
{instance.name}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">
|
||||
{equipmentType?.description}
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs border-[var(--border-default)] text-[var(--text-secondary)]">
|
||||
{equipmentType?.category || 'unknown'}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-[var(--text-muted)] space-y-1 mb-2">
|
||||
<div>
|
||||
Capacity: {instance.usedCapacity}/{instance.totalCapacity}
|
||||
{instance.quality < 100 && (
|
||||
<span className="text-[var(--mana-light)] ml-1">
|
||||
(Quality: {instance.quality}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{instance.enchantments.length > 0 && (
|
||||
<EnchantmentsPanel enchantments={instance.enchantments} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{validSlots.length > 0 && (
|
||||
<EquipControls
|
||||
instance={instance}
|
||||
validSlots={validSlots}
|
||||
onEquip={onEquip}
|
||||
onDelete={onDelete}
|
||||
SLOT_NAMES={SLOT_NAMES}
|
||||
SLOT_ICONS={SLOT_ICONS}
|
||||
/>
|
||||
)}
|
||||
</GameCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getRarityBorderColor(rarity: string) {
|
||||
const colors: Record<string, string> = {
|
||||
common: 'border-[var(--text-muted)]',
|
||||
uncommon: 'border-[var(--color-success)]',
|
||||
rare: 'border-[var(--mana-water)]',
|
||||
epic: 'border-[var(--mana-stellar)]',
|
||||
legendary: 'border-[var(--mana-light)]',
|
||||
mythic: 'border-[var(--mana-dark)]',
|
||||
};
|
||||
return colors[rarity] || 'border-[var(--border-default)]';
|
||||
}
|
||||
|
||||
function getRarityBgColor(rarity: string) {
|
||||
const colors: Record<string, string> = {
|
||||
common: 'bg-[var(--bg-sunken)]/30',
|
||||
uncommon: 'bg-[var(--color-success)]/10',
|
||||
rare: 'bg-[var(--mana-water)]/10',
|
||||
epic: 'bg-[var(--mana-stellar)]/10',
|
||||
legendary: 'bg-[var(--mana-light)]/10',
|
||||
mythic: 'bg-[var(--mana-dark)]/10',
|
||||
};
|
||||
return colors[rarity] || '';
|
||||
}
|
||||
|
||||
function getRarityTextColor(rarity: string) {
|
||||
const colors: Record<string, string> = {
|
||||
common: 'text-[var(--text-secondary)]',
|
||||
uncommon: 'text-[var(--color-success)]',
|
||||
rare: 'text-[var(--mana-water)]',
|
||||
epic: 'text-[var(--mana-stellar)]',
|
||||
legendary: 'text-[var(--mana-light)]',
|
||||
mythic: 'text-[var(--mana-dark)]',
|
||||
};
|
||||
return colors[rarity] || 'text-[var(--text-primary)]';
|
||||
}
|
||||
|
||||
interface EquipControlsProps {
|
||||
instance: EquipmentInstance;
|
||||
validSlots: EquipmentSlot[];
|
||||
onEquip: (instanceId: string, slot: EquipmentSlot) => void;
|
||||
onDelete: (instanceId: string, name: string) => void;
|
||||
SLOT_NAMES: Record<EquipmentSlot, string>;
|
||||
SLOT_ICONS: Record<EquipmentSlot, React.ElementType>;
|
||||
}
|
||||
|
||||
function EquipControls({
|
||||
instance,
|
||||
validSlots,
|
||||
onEquip,
|
||||
onDelete,
|
||||
SLOT_NAMES,
|
||||
SLOT_ICONS,
|
||||
}: EquipControlsProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => onEquip(instance.instanceId, value as EquipmentSlot)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs bg-[var(--bg-sunken)] border-[var(--border-default)]">
|
||||
<SelectValue placeholder="Equip to..." />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-[var(--bg-elevated)] border-[var(--border-default)]">
|
||||
{validSlots.map((slot) => (
|
||||
<SelectItem
|
||||
key={slot}
|
||||
value={slot}
|
||||
className="text-xs text-[var(--text-primary)] focus:bg-[var(--bg-sunken)]"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{(() => {
|
||||
const Icon = SLOT_ICONS[slot];
|
||||
return <Icon size={14} />;
|
||||
})()}
|
||||
{SLOT_NAMES[slot]}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<button
|
||||
className="h-8 w-8 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20 rounded flex items-center justify-center"
|
||||
onClick={() => onDelete(instance.instanceId, instance.name)}
|
||||
aria-label={`Delete ${instance.name}`}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { EquipmentSlot } from '@/lib/game/data/equipment';
|
||||
import { SLOT_NAMES, EQUIPMENT_TYPES } from '@/lib/game/data/equipment';
|
||||
import type { EquipmentInstance } from '@/lib/game/types';
|
||||
import { GameCard } from '@/components/ui/game-card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { AlertCircle, Sword, Shield, HardHat, Shirt, Hand, Footprints, Gem } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { RARITY_BORDER_COLORS, RARITY_BG_COLORS, RARITY_TEXT_COLORS } from './EquipmentTab';
|
||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
||||
import type { EquipmentType } from '@/lib/game/data/equipment';
|
||||
|
||||
const SLOT_ICONS: Record<EquipmentSlot, React.ElementType> = {
|
||||
mainHand: Sword,
|
||||
offHand: Shield,
|
||||
head: HardHat,
|
||||
body: Shirt,
|
||||
hands: Hand,
|
||||
feet: Footprints,
|
||||
accessory1: Gem,
|
||||
accessory2: Gem,
|
||||
};
|
||||
|
||||
interface EquipmentSlotGridProps {
|
||||
equippedInstances: Record<string, string | null>;
|
||||
equipmentInstances: Record<string, EquipmentInstance>;
|
||||
selectedSlot: EquipmentSlot | null;
|
||||
onSlotClick: (slot: EquipmentSlot) => void;
|
||||
onUnequip: (slot: EquipmentSlot) => void;
|
||||
isSlotBlocked: (slot: EquipmentSlot) => boolean;
|
||||
SLOT_GROUPS: Array<{ label: string; slots: EquipmentSlot[] }>;
|
||||
}
|
||||
|
||||
export function EquipmentSlotGrid({
|
||||
equippedInstances,
|
||||
equipmentInstances,
|
||||
selectedSlot,
|
||||
onSlotClick,
|
||||
onUnequip,
|
||||
isSlotBlocked,
|
||||
SLOT_GROUPS,
|
||||
}: EquipmentSlotGridProps) {
|
||||
const renderSlot = (slot: EquipmentSlot) => {
|
||||
const instanceId = equippedInstances[slot];
|
||||
const instance = instanceId ? equipmentInstances[instanceId] : null;
|
||||
const equipmentType = instance ? EQUIPMENT_TYPES?.[instance.typeId] : null;
|
||||
const blocked = isSlotBlocked(slot);
|
||||
const isEmpty = !instance;
|
||||
const SlotIcon = SLOT_ICONS[slot];
|
||||
|
||||
const slotContent = (
|
||||
<GameCard
|
||||
variant={blocked ? 'danger' : instance ? 'default' : 'sunken'}
|
||||
className={`relative transition-all duration-200
|
||||
${isEmpty && !blocked ? 'border-dashed' : ''}
|
||||
${blocked ? 'opacity-60 cursor-not-allowed' : 'hover:border-[var(--border-default)]'}
|
||||
`}
|
||||
role="button"
|
||||
aria-label={`${SLOT_NAMES[slot]} slot${blocked ? ' (blocked by 2-handed weapon)' : ''}${instance ? `: ${instance.name}` : ' (empty)'}`}
|
||||
tabIndex={blocked ? -1 : 0}
|
||||
onClick={() => !blocked && onSlotClick(slot)}
|
||||
onKeyDown={(e) => {
|
||||
if (!blocked && (e.key === 'Enter' || e.key === ' ')) {
|
||||
onSlotClick(slot);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<SlotIcon
|
||||
size={16}
|
||||
className={blocked ? 'text-[var(--text-disabled)]' : 'text-[var(--text-secondary)]'}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm font-semibold
|
||||
${blocked ? 'text-[var(--text-disabled)]' : 'text-[var(--text-primary)]'}
|
||||
`}
|
||||
>
|
||||
{SLOT_NAMES[slot]}
|
||||
</span>
|
||||
{blocked && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs border-[var(--mana-dark)] text-[var(--mana-dark)] ml-2"
|
||||
>
|
||||
<AlertCircle size={12} className="mr-1" />
|
||||
Occupied — 2H Weapon
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{instance && !blocked && (
|
||||
<button
|
||||
className="h-6 w-6 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20 rounded flex items-center justify-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUnequip(slot);
|
||||
}}
|
||||
aria-label={`Unequip ${instance.name}`}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{instance ? (
|
||||
<EquipmentItemDisplay
|
||||
instance={instance}
|
||||
equipmentType={equipmentType}
|
||||
isTwoHanded={equipmentType?.twoHanded || false}
|
||||
isCompact={true}
|
||||
/>
|
||||
) : blocked ? (
|
||||
<div className="text-sm text-[var(--text-disabled)] italic">
|
||||
<AlertCircle size={14} className="inline mr-1" />
|
||||
Blocked by 2-handed weapon
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-[var(--text-muted)] italic text-center py-2">
|
||||
{SLOT_NAMES[slot]}
|
||||
</div>
|
||||
)}
|
||||
</GameCard>
|
||||
);
|
||||
|
||||
if (blocked) {
|
||||
return (
|
||||
<TooltipProvider key={slot}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{slotContent}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
||||
<p>The offhand slot is blocked because a 2-handed weapon is equipped in the main hand.</p>
|
||||
<p className="text-[var(--text-muted)] text-xs mt-1">Unequip the 2-handed weapon to use this slot.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <div key={slot}>{slotContent}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{SLOT_GROUPS.map((group) => (
|
||||
<div key={group.label}>
|
||||
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-2">
|
||||
{group.label}
|
||||
</h4>
|
||||
<div className={`grid gap-3
|
||||
grid-cols-2
|
||||
${group.slots.includes('mainHand' as EquipmentSlot) ? 'sm:grid-cols-2' : 'sm:grid-cols-2 lg:grid-cols-4'}
|
||||
`}>
|
||||
{group.slots.map((slot) => renderSlot(slot))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EquipmentItemDisplayProps {
|
||||
instance: EquipmentInstance;
|
||||
equipmentType: EquipmentType | undefined;
|
||||
isTwoHanded: boolean;
|
||||
isCompact?: boolean;
|
||||
}
|
||||
|
||||
function EquipmentItemDisplay({
|
||||
instance,
|
||||
equipmentType,
|
||||
isTwoHanded,
|
||||
isCompact = false,
|
||||
}: EquipmentItemDisplayProps) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className={`font-semibold text-sm ${RARITY_TEXT_COLORS[instance.rarity] || 'text-[var(--text-primary)]'}`}>
|
||||
{instance.name}
|
||||
{isTwoHanded && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-2 text-xs border-[var(--mana-light)] text-[var(--mana-light)]"
|
||||
>
|
||||
2-Handed
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">
|
||||
Enchantments: {instance.enchantments.length}/{instance.totalCapacity}
|
||||
</div>
|
||||
{instance.enchantments.length > 0 && (
|
||||
<EnchantmentsDisplay enchantments={instance.enchantments} compact={isCompact} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EnchantmentsDisplayProps {
|
||||
enchantments: Array<{ effectId: string; stacks: number }>;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
function EnchantmentsDisplay({ enchantments, compact = false }: EnchantmentsDisplayProps) {
|
||||
return (
|
||||
<div className={`flex flex-wrap gap-1 ${compact ? 'mt-1' : ''}`}>
|
||||
{enchantments.map((ench, i) => {
|
||||
const effect = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||
return (
|
||||
<TooltipProvider key={i}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs cursor-help border-[var(--border-default)] text-[var(--text-secondary)]"
|
||||
>
|
||||
{effect?.name || ench.effectId}
|
||||
{ench.stacks > 1 && ` x${ench.stacks}`}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
||||
<p>{effect?.description || 'Unknown effect'}</p>
|
||||
<p className="text-[var(--text-muted)] text-xs">
|
||||
Category: {effect?.category || 'unknown'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,386 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useMemo } from 'react';
|
||||
import {
|
||||
EQUIPMENT_TYPES,
|
||||
EQUIPMENT_SLOTS,
|
||||
SLOT_NAMES,
|
||||
getEquipmentBySlot,
|
||||
type EquipmentSlot,
|
||||
type EquipmentType,
|
||||
} from '@/lib/game/data/equipment';
|
||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { GameCard } from '@/components/ui/game-card';
|
||||
import { SectionHeader } from '@/components/ui/section-header';
|
||||
import { StatRow } from '@/components/ui/stat-row';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { EquipmentInstance } from '@/lib/game/types';
|
||||
import { EquipmentSlotGrid } from './EquipmentSlotGrid';
|
||||
import { EquipmentInventory } from './EquipmentInventory';
|
||||
import { EnchantmentsPanel } from './EnchantmentsPanel';
|
||||
import { useGameToast } from '@/components/game/GameToast';
|
||||
import { ConfirmDialog } from '@/components/game/ConfirmDialog';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
import { equipItem, unequipItem, deleteEquipmentInstance } from '@/lib/game/crafting-actions';
|
||||
import { useCombatStore, useCraftingStore } from '@/lib/game/stores';
|
||||
import type { GameState } from '@/lib/game/types';
|
||||
|
||||
// Rarity color mappings using design system tokens
|
||||
export const RARITY_BORDER_COLORS: Record<string, string> = {
|
||||
common: 'border-[var(--text-muted)]',
|
||||
uncommon: 'border-[var(--color-success)]',
|
||||
rare: 'border-[var(--mana-water)]',
|
||||
epic: 'border-[var(--mana-stellar)]',
|
||||
legendary: 'border-[var(--mana-light)]',
|
||||
mythic: 'border-[var(--mana-dark)]',
|
||||
};
|
||||
export const RARITY_BG_COLORS: Record<string, string> = {
|
||||
common: 'bg-[var(--bg-sunken)]/30',
|
||||
uncommon: 'bg-[var(--color-success)]/10',
|
||||
rare: 'bg-[var(--mana-water)]/10',
|
||||
epic: 'bg-[var(--mana-stellar)]/10',
|
||||
legendary: 'bg-[var(--mana-light)]/10',
|
||||
mythic: 'bg-[var(--mana-dark)]/10',
|
||||
};
|
||||
export const RARITY_TEXT_COLORS: Record<string, string> = {
|
||||
common: 'text-[var(--text-secondary)]',
|
||||
uncommon: 'text-[var(--color-success)]',
|
||||
rare: 'text-[var(--mana-water)]',
|
||||
epic: 'text-[var(--mana-stellar)]',
|
||||
legendary: 'text-[var(--mana-light)]',
|
||||
mythic: 'text-[var(--mana-dark)]',
|
||||
};
|
||||
|
||||
const SLOT_ICONS: Record<EquipmentSlot, React.ElementType> = {
|
||||
mainHand: () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M14.5 4H5.5L2 10v10a2 2 0 002 2h16a2 2 0 002-2V10l-3.5-6z" />
|
||||
<line x1="6" y1="10" x2="18" y2="10" />
|
||||
</svg>
|
||||
),
|
||||
offHand: () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M20 7H9a2 2 0 01-2-2V4a2 2 0 012-2h5l3 6-3 6h-5a2 2 0 01-2 2v4a2 2 0 002 2h7l3 6-3 6H4a2 2 0 01-2-2v-2" />
|
||||
</svg>
|
||||
),
|
||||
head: () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
||||
<path d="M14 2v6h6M10 14l2 2 4-4" />
|
||||
</svg>
|
||||
),
|
||||
body: () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M20.38 8.5c.6-1.4 1.3-4.9-1.2-8.5-1.1-1.8-3.6-1.8-4.7 0-2.5 3.6-1.9 7.1-1.2 8.5.7 1.4 2.5 1.9 4 1.9 1.5 0 3.3-.5 4-1.9z" />
|
||||
<path d="M12 8v7M8 18h8" />
|
||||
</svg>
|
||||
),
|
||||
hands: () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 11V6a2 2 0 00-2-2h-2a2 2 0 00-2 2v5M6 11v6a2 2 0 002 2h5a2 2 0 002-2v-6" />
|
||||
<path d="M10 15V7a2 2 0 012-2h2a2 2 0 012 2v8" />
|
||||
</svg>
|
||||
),
|
||||
feet: () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 2v20M5 13a4 4 0 000 8h14a4 4 0 000-8" />
|
||||
<path d="M9 13V5a2 2 0 012-2h2a2 2 0 012 2v8" />
|
||||
</svg>
|
||||
),
|
||||
accessory1: () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="8" />
|
||||
<path d="M12 4v8l2 4" />
|
||||
</svg>
|
||||
),
|
||||
accessory2: () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<circle cx="12" cy="12" r="8" />
|
||||
<path d="M12 4v8l2 4" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
// Slot grouping for visual layout
|
||||
type SlotGroup = {
|
||||
label: string;
|
||||
slots: EquipmentSlot[];
|
||||
};
|
||||
|
||||
const SLOT_GROUPS: SlotGroup[] = [
|
||||
{ label: 'Weapon & Shield', slots: ['mainHand', 'offHand'] },
|
||||
{ label: 'Armor', slots: ['head', 'body', 'hands', 'feet'] },
|
||||
{ label: 'Accessories', slots: ['accessory1', 'accessory2'] },
|
||||
];
|
||||
|
||||
export function EquipmentTab() {
|
||||
const showToast = useGameToast();
|
||||
const [selectedSlot, setSelectedSlot] = useState<EquipmentSlot | null>(null);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{ instanceId: string; name: string } | null>(null);
|
||||
|
||||
// Use modular store directly - MUST be called before any conditional returns
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
|
||||
// Get unequipped items - hooks must be called before conditional returns
|
||||
const equippedIds = useMemo(() =>
|
||||
new Set(Object.values(equippedInstances || {}).filter(Boolean)),
|
||||
[equippedInstances]
|
||||
);
|
||||
|
||||
const unequippedItems = useMemo(() =>
|
||||
Object.values(equipmentInstances || {}).filter(
|
||||
(inst) => !equippedIds.has(inst.instanceId)
|
||||
),
|
||||
[equipmentInstances, equippedIds]
|
||||
);
|
||||
|
||||
// Guard against undefined during initialization - AFTER all hooks
|
||||
if (!equippedInstances || !equipmentInstances) {
|
||||
return (
|
||||
<DebugName name="EquipmentTab">
|
||||
<div className="p-4 text-center text-[var(--text-muted)]">
|
||||
Loading equipment data...
|
||||
</div>
|
||||
|
||||
</DebugName>);
|
||||
}
|
||||
|
||||
// Equip an item to a slot
|
||||
const handleEquip = (instanceId: string, slot: EquipmentSlot) => {
|
||||
const instance = equipmentInstances[instanceId];
|
||||
equipItem(instanceId, slot, useCraftingStore.getState as () => GameState, (fn) => useCraftingStore.setState(fn as any));
|
||||
setSelectedSlot(null);
|
||||
showToast('success', 'Item Equipped', `${instance?.name || 'Item'} equipped to ${SLOT_NAMES[slot]}`);
|
||||
};
|
||||
|
||||
// Unequip from a slot
|
||||
const handleUnequip = (slot: EquipmentSlot) => {
|
||||
const instanceId = equippedInstances[slot];
|
||||
const instance = instanceId ? equipmentInstances[instanceId] : null;
|
||||
unequipItem(slot, (fn) => useCraftingStore.setState(fn as any));
|
||||
showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed from ${SLOT_NAMES[slot]}`);
|
||||
};
|
||||
|
||||
// Check if a slot is blocked by a 2-handed weapon
|
||||
const isSlotBlocked = (slot: EquipmentSlot): boolean => {
|
||||
if (slot === 'offHand' && equippedInstances.mainHand) {
|
||||
const mainHandInstance = equipmentInstances[equippedInstances.mainHand];
|
||||
if (!mainHandInstance) return false;
|
||||
const mainHandType = EQUIPMENT_TYPES[mainHandInstance.typeId];
|
||||
return mainHandType?.twoHanded === true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Get items that can be equipped in a slot
|
||||
const getEquippableItems = (slot: EquipmentSlot): EquipmentInstance[] => {
|
||||
const equipmentTypes = getEquipmentBySlot(slot);
|
||||
const typeIds = new Set(equipmentTypes.map((t) => t.id));
|
||||
return unequippedItems.filter((inst) => typeIds.has(inst.typeId));
|
||||
};
|
||||
|
||||
// Get all items that can go in a slot
|
||||
const getItemsForSlot = (slot: EquipmentSlot): EquipmentInstance[] => {
|
||||
if (isSlotBlocked(slot)) return [];
|
||||
|
||||
if (slot === 'accessory1' || slot === 'accessory2') {
|
||||
const accessoryTypeIds = Object.values(EQUIPMENT_TYPES)
|
||||
.filter((t) => t.category === 'accessory')
|
||||
.map((t) => t.id);
|
||||
return unequippedItems.filter((inst) => accessoryTypeIds.includes(inst.typeId));
|
||||
}
|
||||
|
||||
if (slot === 'offHand') {
|
||||
return getEquippableItems(slot).filter((inst) => {
|
||||
const type = EQUIPMENT_TYPES[inst.typeId];
|
||||
return !type?.twoHanded;
|
||||
});
|
||||
}
|
||||
|
||||
return getEquippableItems(slot);
|
||||
};
|
||||
|
||||
// Check if an instance is currently equipped
|
||||
const isEquipped = (instanceId: string): boolean =>
|
||||
Object.values(equippedInstances || {}).includes(instanceId);
|
||||
|
||||
// Get all slots an item type can be equipped to
|
||||
const getEquippableSlots = (typeId: string): EquipmentSlot[] => {
|
||||
const equipmentType = EQUIPMENT_TYPES[typeId];
|
||||
if (!equipmentType) return [];
|
||||
if (equipmentType.category === 'accessory') {
|
||||
return ['accessory1', 'accessory2'];
|
||||
}
|
||||
return [equipmentType.slot];
|
||||
};
|
||||
|
||||
// Handle item deletion
|
||||
const handleDelete = (instanceId: string, name: string) => {
|
||||
setDeleteConfirm({ instanceId, name });
|
||||
};
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (deleteConfirm) {
|
||||
deleteEquipmentInstance(deleteConfirm.instanceId, useCraftingStore.getState, (fn) => useCraftingStore.setState(fn));
|
||||
showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`);
|
||||
setDeleteConfirm(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Use already-fetched values for unified effects
|
||||
const unifiedEffects = getUnifiedEffects({ equipmentInstances, equippedInstances });
|
||||
|
||||
return (
|
||||
<DebugName name="EquipmentTab">
|
||||
<div className="space-y-4 max-w-full overflow-x-hidden">
|
||||
{/* Equipment Slots */}
|
||||
<GameCard variant="default">
|
||||
<SectionHeader
|
||||
title="Equipped Gear"
|
||||
action={
|
||||
<span className="text-xs text-[var(--text-muted)]">
|
||||
{Object.values(equippedInstances || {}).filter(Boolean).length} / {EQUIPMENT_SLOTS.length} slots filled
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<EquipmentSlotGrid
|
||||
equippedInstances={equippedInstances}
|
||||
equipmentInstances={equipmentInstances}
|
||||
selectedSlot={selectedSlot}
|
||||
onSlotClick={setSelectedSlot}
|
||||
onUnequip={handleUnequip}
|
||||
isSlotBlocked={isSlotBlocked}
|
||||
SLOT_GROUPS={SLOT_GROUPS}
|
||||
SLOT_NAMES={SLOT_NAMES}
|
||||
SLOT_ICONS={SLOT_ICONS}
|
||||
/>
|
||||
</GameCard>
|
||||
|
||||
{/* Equipment Inventory */}
|
||||
<GameCard variant="default">
|
||||
<SectionHeader
|
||||
title={`Equipment Inventory (${unequippedItems.length} items)`}
|
||||
/>
|
||||
{unequippedItems.length === 0 ? (
|
||||
<div className="text-[var(--text-muted)] text-sm text-center py-4" role="status">
|
||||
No unequipped items. Craft new gear in the Crafting tab.
|
||||
</div>
|
||||
) : (
|
||||
<EquipmentInventory
|
||||
unequippedItems={unequippedItems}
|
||||
equipmentInstances={equipmentInstances}
|
||||
onEquip={handleEquip}
|
||||
onDelete={handleDelete}
|
||||
getEquippableSlots={getEquippableSlots}
|
||||
SLOT_NAMES={SLOT_NAMES}
|
||||
SLOT_ICONS={SLOT_ICONS}
|
||||
/>
|
||||
)}
|
||||
</GameCard>
|
||||
|
||||
{/* Equipment Stats Summary */}
|
||||
<GameCard variant="default">
|
||||
<SectionHeader title="Equipment Stats Summary" />
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-[var(--mana-light)] font-[var(--font-mono)]">
|
||||
{Object.values(equipmentInstances || {}).length}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">Total Items</div>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-[var(--color-success)] font-[var(--font-mono)]">
|
||||
{equippedIds.size}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">Equipped</div>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-[var(--mana-water)] font-[var(--font-mono)]">
|
||||
{unequippedItems.length}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">In Inventory</div>
|
||||
</div>
|
||||
<div className="p-3 bg-[var(--bg-sunken)]/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-[var(--mana-stellar)] font-[var(--font-mono)]">
|
||||
{Object.values(equipmentInstances || {}).reduce(
|
||||
(sum, inst) => sum + inst.enchantments.length,
|
||||
0
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">Total Enchantments</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enchantment Power */}
|
||||
<GameCard className="mt-4">
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] flex items-center gap-2">
|
||||
✨ Enchantment Power
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
{(() => {
|
||||
const effects = unifiedEffects;
|
||||
if (!effects) return null;
|
||||
const enchantPower = effects.enchantmentPowerMultiplier || 1;
|
||||
return (
|
||||
<>
|
||||
<StatRow
|
||||
label="Enchantment Power:"
|
||||
value={`${enchantPower.toFixed(2)}×`}
|
||||
highlight={enchantPower > 1 ? 'success' : 'default'}
|
||||
/>
|
||||
<p className="text-xs text-[var(--text-muted)] mt-2">
|
||||
Increases the power of all enchantments by {(enchantPower - 1) * 100}%. Multiplier applied to all enchantment effects.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
{/* Active Effects from Equipment */}
|
||||
<div className="mt-4">
|
||||
<div className="text-sm text-[var(--text-muted)] mb-2">Active Effects from Equipment:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(() => {
|
||||
const effects = unifiedEffects;
|
||||
if (!effects?.equipmentEffects) {
|
||||
return <span className="text-[var(--text-muted)] text-sm">No active effects</span>;
|
||||
}
|
||||
const effectEntries = Object.entries(effects.equipmentEffects).filter(([, v]) => v > 0);
|
||||
|
||||
if (effectEntries.length === 0) {
|
||||
return <span className="text-[var(--text-muted)] text-sm">No active effects</span>;
|
||||
}
|
||||
|
||||
return effectEntries.map(([stat, value]) => (
|
||||
<Badge key={stat} variant="outline" className="text-xs border-[var(--border-default)] text-[var(--text-secondary)]">
|
||||
{stat}: +{fmt(value)}
|
||||
</Badge>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<ConfirmDialog
|
||||
open={!!deleteConfirm}
|
||||
onOpenChange={() => setDeleteConfirm(null)}
|
||||
title="Discard Item?"
|
||||
description={`Discard ${deleteConfirm?.name}? This cannot be undone.`}
|
||||
variant="danger"
|
||||
confirmText="Discard"
|
||||
onConfirm={confirmDelete}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</DebugName>);
|
||||
}
|
||||
|
||||
EquipmentTab.displayName = 'EquipmentTab';
|
||||
@@ -1,217 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ChevronUp, ChevronDown, Mountain, Skull } from 'lucide-react';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
|
||||
interface FloorControlsProps {
|
||||
// Store values passed as individual props
|
||||
currentFloor: number;
|
||||
floorHP: number;
|
||||
floorMaxHP: number;
|
||||
maxFloorReached: number;
|
||||
equipmentSpellStates: any[];
|
||||
|
||||
// Other props
|
||||
climbDirection: 'up' | 'down' | null;
|
||||
isGuardianFloor: boolean;
|
||||
currentRoom: any;
|
||||
currentGuardian: any;
|
||||
isFloorCleared: boolean;
|
||||
floorElemDef: any;
|
||||
roomType: string;
|
||||
roomConfig: { label: string; icon: string; color: string };
|
||||
activeEquipmentSpells: any[];
|
||||
floorElem: string;
|
||||
totalDPS: number;
|
||||
calcDamage: (state: { skills: Record<string, number>; signedPacts: number[] }, spellId: string, floorElem?: string) => number;
|
||||
SPELLS_DEF: Record<string, any>;
|
||||
canCastSpell: (spellId: string) => boolean;
|
||||
storeCurrentAction: string;
|
||||
handleClimb: (direction: 'up' | 'down') => void;
|
||||
formatSpellCost: (cost: any) => string;
|
||||
getSpellCostColor: (cost: any) => string;
|
||||
// Skills and pacts needed for calcDamage
|
||||
skills: Record<string, number>;
|
||||
signedPacts: number[];
|
||||
}
|
||||
|
||||
export function FloorControls({
|
||||
currentFloor,
|
||||
floorHP,
|
||||
floorMaxHP,
|
||||
maxFloorReached,
|
||||
equipmentSpellStates,
|
||||
climbDirection,
|
||||
isGuardianFloor,
|
||||
currentRoom,
|
||||
currentGuardian,
|
||||
isFloorCleared,
|
||||
floorElemDef,
|
||||
roomType,
|
||||
roomConfig,
|
||||
activeEquipmentSpells,
|
||||
floorElem,
|
||||
totalDPS,
|
||||
calcDamage,
|
||||
SPELLS_DEF,
|
||||
canCastSpell,
|
||||
storeCurrentAction,
|
||||
handleClimb,
|
||||
formatSpellCost,
|
||||
getSpellCostColor,
|
||||
skills,
|
||||
signedPacts,
|
||||
}: FloorControlsProps) {
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Floor Navigation</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleClimb('up')}
|
||||
disabled={storeCurrentAction === 'climb' || isFloorCleared || maxFloorReached >= 100}
|
||||
className="border-gray-600 hover:bg-gray-800"
|
||||
>
|
||||
<ChevronUp className="w-4 h-4 mr-1" />
|
||||
Climb Up
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleClimb('down')}
|
||||
disabled={storeCurrentAction === 'climb' || currentFloor <= 1}
|
||||
className="border-gray-600 hover:bg-gray-800"
|
||||
>
|
||||
<ChevronDown className="w-4 h-4 mr-1" />
|
||||
Climb Down
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{storeCurrentAction === 'climb' && (
|
||||
<div className="p-3 bg-amber-900/30 rounded border border-amber-700/50">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Mountain className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-sm font-semibold text-amber-400">
|
||||
Climbing {climbDirection === 'up' ? 'Up' : 'Down'}
|
||||
</span>
|
||||
{isGuardianFloor && (
|
||||
<Badge className="bg-red-900/50 text-red-300 border-red-600">GUARDIAN</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{currentGuardian && (
|
||||
<div className="text-xs mb-2 p-2 bg-gray-800/50 rounded">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skull className="w-3 h-3 text-red-400" />
|
||||
<span className="font-semibold" style={{ color: floorElemDef?.color }}>
|
||||
{currentGuardian.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!(roomType === 'swarm' || roomType === 'puzzle') && (
|
||||
<div className="space-y-1 mb-2">
|
||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, (floorHP / floorMaxHP) * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
||||
boxShadow: `0 0 8px ${floorElemDef?.glow}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>{fmt(floorHP)} / {fmt(floorMaxHP)} HP</span>
|
||||
<span className="text-amber-400">
|
||||
DPS: {activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeEquipmentSpells.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) return null;
|
||||
const spellState = equipmentSpellStates?.find(
|
||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
||||
);
|
||||
const progress = spellState?.castProgress || 0;
|
||||
const canCast = canCastSpell(spellId);
|
||||
|
||||
return (
|
||||
<div key={`${spellId}-${equipmentId}`} className="p-2 bg-gray-800/50 rounded">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
||||
{spellDef.name}
|
||||
</span>
|
||||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>
|
||||
{canCast ? '✓' : '✗'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mb-1">
|
||||
⚔️ {fmt(calcDamage({ skills, signedPacts }, spellId, floorElem))} dmg • {' '}
|
||||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
||||
{formatSpellCost(spellDef.cost)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>Cast</span>
|
||||
<span>{(progress * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, progress * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color}99, ${spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-500 italic">No active spells. Equip staves with spell effects.</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{storeCurrentAction !== 'climb' && (
|
||||
<div className="text-xs text-gray-500 text-center">
|
||||
Click Climb Up/Down to begin climbing
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function fmt(value: number): string {
|
||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||||
return value.toFixed(0);
|
||||
}
|
||||
|
||||
function fmtDec(value: number): string {
|
||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||||
return value.toFixed(0);
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { GameCard, StatRow, ElementBadge, ActionButton } from '@/components/ui';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Mountain, Zap, Clock, Swords, Sparkles, Lock, Check, X,
|
||||
Info, HelpCircle
|
||||
} from 'lucide-react';
|
||||
import { GOLEMS_DEF, getGolemSlots, isGolemUnlocked, getGolemDamage, getGolemAttackSpeed, getGolemFloorDuration } from '@/lib/game/data/golems';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { useManaStore, useSkillStore, useCombatStore, useAttunementStore } from '@/lib/game/stores';
|
||||
|
||||
export function GolemancyTab() {
|
||||
const attunements = useAttunementStore((s) => s.attunements);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const golemancy = useCombatStore((s) => s.golemancy);
|
||||
const currentFloor = useCombatStore((s) => s.currentFloor);
|
||||
const currentRoom = useCombatStore((s) => s.currentRoom);
|
||||
const toggleGolem = useCombatStore((s) => s.toggleGolem);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
|
||||
// Get Fabricator level and golem slots
|
||||
const fabricatorLevel = attunements.fabricator?.level || 0;
|
||||
const fabricatorActive = attunements.fabricator?.active || false;
|
||||
const maxSlots = getGolemSlots(fabricatorLevel);
|
||||
|
||||
// Get unlocked elements
|
||||
const unlockedElements = Object.entries(elements)
|
||||
.filter(([, e]) => e.unlocked)
|
||||
.map(([id]) => id);
|
||||
|
||||
// Get all unlocked golems
|
||||
const unlockedGolems = Object.values(GOLEMS_DEF || {}).filter(golem =>
|
||||
isGolemUnlocked(golem.id, attunements, unlockedElements)
|
||||
);
|
||||
|
||||
// Check if golemancy is available
|
||||
const hasGolemancy = fabricatorActive && fabricatorLevel >= 2;
|
||||
|
||||
// Check if currently in combat (not puzzle)
|
||||
const inCombat = currentRoom?.roomType !== 'puzzle';
|
||||
|
||||
// Get element info helper
|
||||
const getElementInfo = (elementId: string) => {
|
||||
return ELEMENTS[elementId];
|
||||
};
|
||||
|
||||
// Render a golem card
|
||||
const renderGolemCard = (golemId: string, isUnlocked: boolean) => {
|
||||
const golem = GOLEMS_DEF[golemId];
|
||||
if (!golem) return null;
|
||||
|
||||
const isEnabled = golemancy.enabledGolems.includes(golemId);
|
||||
const isSelected = golemancy.summonedGolems.some(g => g.golemId === golemId);
|
||||
|
||||
// Calculate effective stats
|
||||
const damage = getGolemDamage(golemId, skills);
|
||||
const attackSpeed = getGolemAttackSpeed(golemId, skills);
|
||||
const floorDuration = getGolemFloorDuration(skills);
|
||||
|
||||
// Get element color
|
||||
const primaryElement = getElementInfo(golem.baseManaType);
|
||||
const elementId = golem.baseManaType;
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Locked golem card
|
||||
return (
|
||||
|
||||
<DebugName name="GolemancyTab">
|
||||
<GameCard key={golemId} variant="sunken" className="opacity-60">
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
||||
<Lock className="w-4 h-4" />
|
||||
<span className="text-[var(--text-muted)]">???</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-muted)]">
|
||||
{golem.unlockCondition.type === 'attunement_level' && (
|
||||
<div>Requires Fabricator Level {golem.unlockCondition.level}</div>
|
||||
)}
|
||||
{golem.unlockCondition.type === 'mana_unlocked' && (
|
||||
<div>Requires {ELEMENTS[golem.unlockCondition.manaType || '']?.name || golem.unlockCondition.manaType} Mana</div>
|
||||
)}
|
||||
{golem.unlockCondition.type === 'dual_attunement' && (
|
||||
<div>Requires Enchanter & Fabricator Level 5</div>
|
||||
)}
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
</DebugName>);
|
||||
}
|
||||
|
||||
return (
|
||||
<GameCard
|
||||
key={golemId}
|
||||
variant={isEnabled ? "default" : "sunken"}
|
||||
className={`transition-all cursor-pointer border-2 ${
|
||||
isEnabled
|
||||
? 'border-[var(--color-success)] bg-[var(--bg-surface)]'
|
||||
: 'border-[var(--border-subtle)] hover:border-[var(--border-default)]'
|
||||
}`}
|
||||
onClick={() => toggleGolem(golemId)}
|
||||
aria-label={`${isEnabled ? 'Disable' : 'Enable'} ${golem.name}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mountain className="w-4 h-4" style={{ color: `var(--mana-${elementId})` }} />
|
||||
<span style={{ color: `var(--mana-${elementId})` }}>{golem.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{golem.isAoe && (
|
||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
||||
AOE {golem.aoeTargets}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
||||
{golem.tier}
|
||||
</span>
|
||||
{isEnabled ? (
|
||||
<Check className="w-4 h-4 text-[var(--color-success)]" />
|
||||
) : (
|
||||
<X className="w-4 h-4 text-[var(--text-muted)]" />
|
||||
)}
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-[var(--text-secondary)]">{golem.description}</p>
|
||||
|
||||
<Separator className="bg-[var(--border-subtle)]" />
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<StatRow label="DMG:" value={damage.toString()} />
|
||||
<StatRow label="Speed:" value={`${attackSpeed.toFixed(1)}/hr`} />
|
||||
<StatRow label="Pierce:" value={`${Math.floor(golem.armorPierce * 100)}%`} />
|
||||
<StatRow label="Duration:" value={`${floorDuration} floor(s)`} />
|
||||
</div>
|
||||
|
||||
<Separator className="bg-[var(--border-subtle)]" />
|
||||
|
||||
{/* Summon Cost */}
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">Summon Cost:</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{golem.summonCost.map((cost, idx) => {
|
||||
const elem = getElementInfo(cost.element || '');
|
||||
const available = cost.type === 'raw'
|
||||
? rawMana
|
||||
: elements[cost.element || '']?.current || 0;
|
||||
const canAfford = available >= cost.amount;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={idx}
|
||||
className={`text-xs px-1.5 py-0.5 border rounded ${
|
||||
canAfford
|
||||
? 'border-[var(--color-success)] text-[var(--color-success)]'
|
||||
: 'border-[var(--color-danger)] text-[var(--color-danger)]'
|
||||
}`}
|
||||
>
|
||||
{cost.element && <ElementBadge elementId={cost.element} size="sm" />}
|
||||
{' '}{cost.amount}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Maintenance Cost */}
|
||||
<div>
|
||||
<div className="text-xs text-[var(--text-secondary)] mb-1">Maintenance/hr:</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{golem.maintenanceCost.map((cost, idx) => {
|
||||
return (
|
||||
<span key={idx} className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
||||
{cost.element && <ElementBadge elementId={cost.element} size="sm" />}
|
||||
{' '}{cost.amount}/hr
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
{isSelected && (
|
||||
<div className="mt-2 text-xs text-[var(--color-success)] flex items-center gap-1">
|
||||
<Sparkles className="w-3 h-3" />
|
||||
Active on Floor {currentFloor}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</GameCard>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<GameCard>
|
||||
<div className="pb-2">
|
||||
<h2 className="text-lg font-[var(--font-heading)] font-semibold flex items-center gap-2 text-[var(--text-primary)]">
|
||||
<Mountain className="w-5 h-5 text-[var(--mana-earth)]" />
|
||||
Golemancy
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{!hasGolemancy ? (
|
||||
<div className="text-center text-[var(--text-secondary)] py-4">
|
||||
<Lock className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>Unlock the Fabricator attunement and reach Level 2 to summon golems.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<StatRow
|
||||
label="Golem Slots:"
|
||||
value={`${golemancy.enabledGolems.length} / ${maxSlots}`}
|
||||
highlight={golemancy.enabledGolems.length > 0 ? 'success' : undefined}
|
||||
/>
|
||||
<StatRow
|
||||
label="Fabricator Level:"
|
||||
value={fabricatorLevel.toString()}
|
||||
highlight="warning"
|
||||
/>
|
||||
<StatRow
|
||||
label="Floor Duration:"
|
||||
value={`${getGolemFloorDuration(skills)} floor(s)`}
|
||||
/>
|
||||
<StatRow
|
||||
label="Status:"
|
||||
value={inCombat ? 'Combat Active' : 'Puzzle Room (No Golems)'}
|
||||
highlight={inCombat ? 'success' : 'warning'}
|
||||
/>
|
||||
|
||||
<p className="text-xs text-[var(--text-muted)] mt-2">
|
||||
Golems are automatically summoned at the start of each combat floor.
|
||||
They cost mana to maintain and will be dismissed if you run out.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GameCard>
|
||||
|
||||
{/* Active Golems - Empty State */}
|
||||
{hasGolemancy && golemancy.summonedGolems.length === 0 && (
|
||||
<GameCard variant="sunken">
|
||||
<div className="text-center py-4 text-[var(--text-muted)]">
|
||||
<Info className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
||||
<p className="text-sm">No golems summoned</p>
|
||||
<p className="text-xs mt-1">Enable golems below to summon them at the start of combat</p>
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Active Golems */}
|
||||
{hasGolemancy && golemancy.summonedGolems.length > 0 && (
|
||||
<GameCard variant="default" className="border-[var(--color-success)]">
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2 text-[var(--color-success)]">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Active Golems ({golemancy.summonedGolems.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{golemancy.summonedGolems.map(sg => {
|
||||
const golem = GOLEMS_DEF[sg.golemId];
|
||||
if (!golem) return null;
|
||||
|
||||
return (
|
||||
<span key={sg.golemId} className="text-xs px-2 py-1 border border-[var(--border-default)] rounded">
|
||||
<Mountain className="w-3 h-3 inline mr-1" style={{ color: `var(--mana-${golem.baseManaType})` }} />
|
||||
{golem.name}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Golem Selection */}
|
||||
{hasGolemancy && (
|
||||
<GameCard>
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Select Golems to Summon</h3>
|
||||
</div>
|
||||
<ScrollArea className="h-96">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 pr-4">
|
||||
{/* Unlocked Golems */}
|
||||
{unlockedGolems.map(golem => renderGolemCard(golem.id, true))}
|
||||
|
||||
{/* Locked Golems */}
|
||||
{Object.values(GOLEMS_DEF || {})
|
||||
.filter(g => !isGolemUnlocked(g.id, attunements, unlockedElements))
|
||||
.map(golem => renderGolemCard(golem.id, false))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</GameCard>
|
||||
)}
|
||||
|
||||
{/* Golemancy Skills Info */}
|
||||
<GameCard>
|
||||
<div className="pb-2">
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)]">Golemancy Skills</h3>
|
||||
</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<StatRow label="Golem Mastery:" value={`+${(skills.golemMastery || 0) * 10}% damage`} />
|
||||
<StatRow label="Golem Efficiency:" value={`+${(skills.golemEfficiency || 0) * 5}% attack speed`} />
|
||||
<StatRow label="Golem Longevity:" value={`+${skills.golemLongevity || 0} floor duration`} />
|
||||
<StatRow label="Golem Siphon:" value={`-${(skills.golemSiphon || 0) * 10}% maintenance`} />
|
||||
</div>
|
||||
</GameCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
GolemancyTab.displayName = "GolemancyTab";
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
@@ -1,53 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { GUARDIANS } from '@/lib/game/constants';
|
||||
|
||||
interface GuardianPanelProps {
|
||||
currentFloor: number;
|
||||
floorElemDef: any;
|
||||
}
|
||||
|
||||
export function GuardianPanel({ currentFloor, floorElemDef }: GuardianPanelProps) {
|
||||
const guardian = GUARDIANS[currentFloor];
|
||||
if (!guardian) return null;
|
||||
|
||||
return (
|
||||
<div className="p-3 bg-red-900/20 rounded border border-red-700/50 mb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">⚔️</span>
|
||||
<span className="text-sm font-semibold" style={{ color: floorElemDef?.color }}>
|
||||
{guardian.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-xs text-gray-300">
|
||||
<div>Power: <strong className="text-red-400">{fmt(guardian.power)}</strong></div>
|
||||
|
||||
{guardian.armor > 0 && (
|
||||
<div>Armor: <strong>{(guardian.armor * 100).toFixed(0)}%</strong></div>
|
||||
)}
|
||||
|
||||
{guardian.effects && guardian.effects.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{guardian.effects.map((eff: any, i: number) => (
|
||||
<Badge key={i} variant="outline" className="text-xs py-0">
|
||||
{eff.type === 'burn' && `🔥 Burn ${(eff.value * 100).toFixed(0)}%`}
|
||||
{eff.type === 'armor_pierce' && `🗡️ Pierce ${(eff.value * 100).toFixed(0)}%`}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function fmt(value: number): string {
|
||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||||
return value.toFixed(0);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useCraftingStore, useManaStore } from '@/lib/game/stores';
|
||||
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
|
||||
export function LootTab() {
|
||||
const lootInventory = useCraftingStore((s) => s.lootInventory);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
const deleteMaterial = useCraftingStore((s) => s.deleteMaterial);
|
||||
const deleteEquipmentInstance = useCraftingStore((s) => s.deleteEquipmentInstance);
|
||||
|
||||
return (
|
||||
<DebugName name="LootTab">
|
||||
<div className="space-y-4">
|
||||
<LootInventoryDisplay
|
||||
inventory={lootInventory}
|
||||
elements={elements}
|
||||
equipmentInstances={equipmentInstances}
|
||||
onDeleteMaterial={deleteMaterial}
|
||||
onDeleteEquipment={deleteEquipmentInstance}
|
||||
/>
|
||||
</div>
|
||||
</DebugName>
|
||||
);
|
||||
}
|
||||
|
||||
LootTab.displayName = "LootTab";
|
||||
@@ -1,22 +0,0 @@
|
||||
// ─── Milestone Progress ───────────────────────────────────────────
|
||||
// Milestone upgrade progress indicator for skill rows
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface MilestoneProgressProps {
|
||||
milestoneInfo: {
|
||||
milestone: 5 | 10;
|
||||
hasUpgrades: boolean;
|
||||
selectedCount: number;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function MilestoneProgress({ milestoneInfo }: MilestoneProgressProps) {
|
||||
if (!milestoneInfo) return null;
|
||||
|
||||
return (
|
||||
<div className="text-xs text-amber-400 mt-1 flex items-center gap-1">
|
||||
⭐ Level {milestoneInfo.milestone} milestone: {milestoneInfo.selectedCount}/2 upgrades selected
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
// ─── Prestige/Grimoire Tab ──────────────────────────
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { usePrestigeStore, useSkillStore, useManaStore, useCraftingStore } from '@/lib/game/stores';
|
||||
import { useGameLoop } from '@/lib/game/stores/gameHooks';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import {
|
||||
ELEMENTS,
|
||||
GUARDIANS,
|
||||
PRESTIGE_DEF,
|
||||
getStudySpeedMultiplier,
|
||||
} from '@/lib/game/constants';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { fmt } from '@/lib/game/stores';
|
||||
|
||||
export function PrestigeTab() {
|
||||
const [selectedManaType, setSelectedManaType] = useState<string>('');
|
||||
|
||||
useGameLoop();
|
||||
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const doPrestige = usePrestigeStore((s) => s.doPrestige);
|
||||
|
||||
const upgradeEffects = getUnifiedEffects({
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
equipmentInstances,
|
||||
equippedInstances,
|
||||
});
|
||||
|
||||
// Get unlocked elements for mana type selector
|
||||
const unlockedElements = Object.entries(ELEMENTS)
|
||||
.filter(([id]) => elements[id]?.unlocked)
|
||||
.map(([id, elem]) => ({
|
||||
id,
|
||||
name: elem.name,
|
||||
sym: elem.sym,
|
||||
color: elem.color,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* Prestige Upgrades */}
|
||||
{Object.entries(PRESTIGE_DEF || {}).map(([id, def]) => {
|
||||
const level = prestigeUpgrades[id] || 0;
|
||||
const canAfford = rawMana >= def.cost;
|
||||
const effect = upgradeEffects ? upgradeEffects.specials.has(id) : false;
|
||||
|
||||
return (
|
||||
<Card key={id} className={effect ? "border-[var(--color-success)]/50 bg-[var(--color-success)]/10" : "bg-gray-900/80 border-gray-700"}>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm flex items-center gap-2">
|
||||
<span>{def.name}</span>
|
||||
{effect && <Badge className="bg-[var(--color-success)]/20 text-[var(--color-success)]">Active</Badge>}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-xs text-gray-400 mb-2">{def.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-400">Level: {level}/{def.maxLevel}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={!canAfford || level >= def.maxLevel}
|
||||
onClick={() => doPrestige(id)}
|
||||
>
|
||||
{level >= def.maxLevel ? 'Maxed' : `Upgrade (${fmt(def.cost)})`}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Mana Type Selection for Attunements */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Select Mana Type for Attunement</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{unlockedElements.map(elem => (
|
||||
<Button
|
||||
key={elem.id}
|
||||
variant={selectedManaType === elem.id ? "default" : "outline"}
|
||||
onClick={() => setSelectedManaType(elem.id)}
|
||||
className="justify-start"
|
||||
>
|
||||
<span className="mr-2" style={{ color: elem.color }}>{elem.sym}</span>
|
||||
{elem.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
PrestigeTab.displayName = "PrestigeTab";
|
||||
@@ -1,194 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Shield, Wind, Heart, ShieldCheck, Skull } from 'lucide-react';
|
||||
import type { RoomDisplayProps } from '@/lib/game/types';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
|
||||
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
|
||||
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
|
||||
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
|
||||
speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
|
||||
guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
|
||||
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
|
||||
} as const;
|
||||
|
||||
export function RoomDisplay({
|
||||
roomType,
|
||||
roomConfig,
|
||||
primaryEnemy,
|
||||
swarmEnemies,
|
||||
puzzleId,
|
||||
puzzleProgress,
|
||||
simpleMode,
|
||||
floorElemDef,
|
||||
floorHP,
|
||||
floorMaxHP,
|
||||
totalDPS,
|
||||
currentAction,
|
||||
activeEquipmentSpells,
|
||||
}: RoomDisplayProps) {
|
||||
// Puzzle Room Display
|
||||
if (roomType === 'puzzle') {
|
||||
return (
|
||||
<div className="p-3 bg-purple-900/20 rounded border border-purple-700">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">🧩</span>
|
||||
<span className="text-sm font-semibold text-purple-300">
|
||||
{puzzleId ? puzzleId.replace(/_/g, ' ').toUpperCase() : 'Puzzle Room'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>Progress</span>
|
||||
<span>{((puzzleProgress || 0) * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<Progress value={Math.min(100, (puzzleProgress || 0) * 100)} className="h-2 bg-gray-800" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Single Enemy Display (Combat/Speed/Guardian - non-swarm)
|
||||
if ((roomType === 'combat' || roomType === 'speed' || roomType === 'guardian') && primaryEnemy) {
|
||||
return (
|
||||
<div className="p-3 bg-gray-800/50 rounded border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skull className="w-4 h-4 text-red-400" />
|
||||
<span className="text-sm font-semibold text-gray-200">
|
||||
{primaryEnemy.name || 'Unknown Enemy'}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{ELEMENTS[primaryEnemy.element]?.sym} {ELEMENTS[primaryEnemy.element]?.name}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Enemy HP Bar */}
|
||||
<div className="space-y-1 mb-2">
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, (primaryEnemy.hp / primaryEnemy.maxHP) * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
||||
<span>{fmt(primaryEnemy.hp)} / {fmt(primaryEnemy.maxHP)} HP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Properties */}
|
||||
<EnemyProperties enemy={primaryEnemy} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Swarm Enemies Display
|
||||
if (roomType === 'swarm' && swarmEnemies && swarmEnemies.length > 0) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-gray-400 font-semibold">
|
||||
Swarm Enemies ({swarmEnemies.length})
|
||||
</div>
|
||||
{swarmEnemies.map((enemy: any, index: number) => (
|
||||
<div key={enemy.id || `swarm-${index}`} className="p-2 bg-gray-800/50 rounded border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skull className="w-3 h-3 text-red-400" />
|
||||
<span className="text-xs font-semibold text-gray-300">
|
||||
{enemy.name || `Enemy ${index + 1}`}
|
||||
</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs py-0">
|
||||
{ELEMENTS[enemy.element]?.sym} {fmt(enemy.hp)}/{fmt(enemy.maxHP)} HP
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, (enemy.hp / enemy.maxHP) * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${ELEMENTS[enemy.element]?.color}99, ${ELEMENTS[enemy.element]?.color})`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<EnemyProperties enemy={enemy} small />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Floor HP Bar (non-swarm, non-puzzle)
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="h-3 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, (floorHP / floorMaxHP) * 100)}%`,
|
||||
background: `linear-gradient(90deg, ${floorElemDef?.color}99, ${floorElemDef?.color})`,
|
||||
boxShadow: `0 0 10px ${floorElemDef?.glow}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-400 game-mono">
|
||||
<span>{fmt(floorHP)} / {fmt(floorMaxHP)} HP</span>
|
||||
<span>DPS: {currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EnemyProperties({ enemy, small }: { enemy: any; small?: boolean }) {
|
||||
const props = [];
|
||||
if (enemy.armor > 0) props.push({ type: 'armor', label: `${(enemy.armor * 100).toFixed(0)}% Armor`, icon: Shield });
|
||||
if (enemy.dodgeChance > 0) props.push({ type: 'dodge', label: `${(enemy.dodgeChance * 100).toFixed(0)}% Dodge`, icon: Wind });
|
||||
if (enemy.healthRegen > 0) props.push({ type: 'regen', label: `${(enemy.healthRegen * 100).toFixed(1)}% Regen`, icon: Heart });
|
||||
if (enemy.barrier > 0) props.push({ type: 'barrier', label: `${(enemy.barrier * 100).toFixed(0)}% Barrier`, icon: ShieldCheck });
|
||||
|
||||
if (props.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap gap-2 mt-2 ${small ? 'mt-1' : ''}`}>
|
||||
{props.map((p, i) => {
|
||||
const Icon = p.icon;
|
||||
return (
|
||||
<Tooltip key={i}>
|
||||
<TooltipTrigger>
|
||||
<Badge variant="outline" className={`text-xs py-0 ${small ? 'text-xs py-0' : ''}`}>
|
||||
<Icon className={`w-${small ? 2 : 3} h-${small ? 2 : 3} mr-1`} />
|
||||
{p.label}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Reduces incoming damage by {(enemy.armor * 100).toFixed(0)}%</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function fmt(value: number): string {
|
||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||||
return value.toFixed(0);
|
||||
}
|
||||
|
||||
function fmtDec(value: number): string {
|
||||
if (value >= 1e12) return (value / 1e12).toFixed(2) + 't';
|
||||
if (value >= 1e9) return (value / 1e9).toFixed(2) + 'b';
|
||||
if (value >= 1e6) return (value / 1e6).toFixed(2) + 'm';
|
||||
if (value >= 1e3) return (value / 1e3).toFixed(2) + 'k';
|
||||
return value.toFixed(0);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
// ─── Skill Category Header ───────────────────────────────────────────
|
||||
// Header for a skill category with collapse/expand toggle
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
|
||||
interface SkillCategoryHeaderProps {
|
||||
category: {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
};
|
||||
skillCount: number;
|
||||
isCollapsed: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export function SkillCategoryHeader({
|
||||
category,
|
||||
skillCount,
|
||||
isCollapsed,
|
||||
onToggle,
|
||||
}: SkillCategoryHeaderProps) {
|
||||
return (
|
||||
<CardHeader className="pb-2 cursor-pointer" onClick={onToggle}>
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center justify-between">
|
||||
<span>
|
||||
{category.icon} {category.name}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{skillCount} skills
|
||||
</Badge>
|
||||
{isCollapsed ? <ChevronRight className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
);
|
||||
}
|
||||
|
||||
// Local import of CardHeader and CardTitle to avoid circular deps
|
||||
import { CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -1,50 +0,0 @@
|
||||
// ─── Skill Multipliers ───────────────────────────────────────────
|
||||
// Study speed and cost multiplier display for skill rows
|
||||
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
|
||||
type AdditionalCost = { type: 'element'; element: string; amount: number };
|
||||
|
||||
interface SkillMultipliersProps {
|
||||
effectiveStudyTime: number;
|
||||
speedMult: number;
|
||||
costMult: number;
|
||||
cost: number;
|
||||
additionalCost?: AdditionalCost;
|
||||
}
|
||||
|
||||
export function SkillMultipliers({
|
||||
effectiveStudyTime,
|
||||
speedMult,
|
||||
costMult,
|
||||
cost,
|
||||
additionalCost,
|
||||
}: SkillMultipliersProps) {
|
||||
return (
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
<span className={speedMult > 1 ? 'text-green-400' : ''}>
|
||||
Study: {formatStudyTime(effectiveStudyTime)}{speedMult > 1 && (
|
||||
<span className="text-xs ml-1">({Math.round(speedMult * 100)}% speed)</span>
|
||||
)}
|
||||
</span>
|
||||
{' • '}
|
||||
<span className={costMult < 1 ? 'text-green-400' : ''}>
|
||||
Cost: {cost} mana{costMult < 1 && (
|
||||
<span className="text-xs ml-1">({Math.round(costMult * 100)}% cost)</span>
|
||||
)}
|
||||
{additionalCost && additionalCost.type === 'element' && (
|
||||
<span className="ml-2" style={{ color: ELEMENTS[additionalCost.element]?.color }}>
|
||||
+ {additionalCost.amount} {ELEMENTS[additionalCost.element]?.sym} {additionalCost.element}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatStudyTime(ms: number): string {
|
||||
if (ms < 60_000) return `${Math.floor(ms / 1000)}s`;
|
||||
if (ms < 3_600_000) return `${Math.floor(ms / 60_000)}m ${Math.floor((ms % 60_000) / 1000)}s`;
|
||||
if (ms < 86_400_000) return `${Math.floor(ms / 3_600_000)}h ${Math.floor((ms % 3_600_000) / 60_000)}m`;
|
||||
return `${Math.floor(ms / 86_400_000)}d ${Math.floor((ms % 86_400_000) / 3_600_000)}h`;
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
// ─── Skill Row Component ───────────────────────────────────────────────────
|
||||
// Individual skill row for the Skills tab - extracted from SkillsTab for modularity
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { formatStudyTime } from '@/lib/game/formatting';
|
||||
import { getUpgradesForSkillAtMilestone, getNextTierSkill, getTierMultiplier, SKILL_EVOLUTION_PATHS } from '@/lib/game/skill-evolution';
|
||||
import type { ComputedEffects } from '@/lib/game/upgrade-effects.types';
|
||||
import { SPECIAL_EFFECTS, hasSpecial } from '@/lib/game/special-effects';
|
||||
import { MilestoneProgress } from './MilestoneProgress';
|
||||
import { SkillMultipliers } from './SkillMultipliers';
|
||||
|
||||
type StudyTarget = { type: 'skill' | 'spell'; id: string; progress: number; required: number; } | null;
|
||||
|
||||
interface SkillRowProps {
|
||||
skillId: string;
|
||||
def: {
|
||||
name: string;
|
||||
desc: string;
|
||||
cat: string;
|
||||
max: number;
|
||||
studyTime: number;
|
||||
base: number;
|
||||
cost?: { type: 'element'; element: string; amount: number } | { type: 'raw'; amount: number };
|
||||
req?: Record<string, number>;
|
||||
};
|
||||
level: number;
|
||||
maxed: boolean;
|
||||
isStudying: boolean;
|
||||
tierMultiplier: number;
|
||||
skillDisplayName: string;
|
||||
selectedUpgrades: string[];
|
||||
selectedL5: string[];
|
||||
selectedL10: string[];
|
||||
prereqMet: boolean;
|
||||
canStudy: boolean;
|
||||
isParallelStudy: boolean;
|
||||
canParallelStudy: boolean;
|
||||
canTierUp: boolean;
|
||||
hasInsufficientMana: boolean;
|
||||
currentStudyTarget: StudyTarget;
|
||||
milestoneInfo: { milestone: 5 | 10; hasUpgrades: boolean; selectedCount: number } | null;
|
||||
upgradeEffects: ComputedEffects;
|
||||
// Costs and times
|
||||
cost: number;
|
||||
additionalCost?: { type: 'element'; element: string; amount: number };
|
||||
effectiveStudyTime: number;
|
||||
costMult: number;
|
||||
speedMult: number;
|
||||
// Callbacks
|
||||
onStudy: (skillId: string) => void;
|
||||
onParallelStudy: (skillId: string) => void;
|
||||
onCancelStudy: (skillId: string) => void;
|
||||
onUpgradeDialogOpen: (skillId: string, milestone: 5 | 10) => void;
|
||||
onTierUp: (skillId: string) => void;
|
||||
onShowToast: (type: 'info' | 'error', title: string, description: string) => void;
|
||||
tierUpLabel?: string;
|
||||
}
|
||||
|
||||
export function SkillRow(props: SkillRowProps) {
|
||||
const {
|
||||
skillId,
|
||||
def,
|
||||
level,
|
||||
maxed,
|
||||
isStudying,
|
||||
tierMultiplier,
|
||||
skillDisplayName,
|
||||
selectedUpgrades,
|
||||
selectedL5,
|
||||
selectedL10,
|
||||
prereqMet,
|
||||
canStudy,
|
||||
isParallelStudy,
|
||||
canParallelStudy,
|
||||
canTierUp,
|
||||
hasInsufficientMana,
|
||||
currentStudyTarget,
|
||||
milestoneInfo,
|
||||
upgradeEffects,
|
||||
cost,
|
||||
additionalCost,
|
||||
effectiveStudyTime,
|
||||
costMult,
|
||||
speedMult,
|
||||
onStudy,
|
||||
onParallelStudy,
|
||||
onCancelStudy,
|
||||
onUpgradeDialogOpen,
|
||||
onTierUp,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={skillId}
|
||||
className={`flex flex-col sm:flex-row sm:items-center justify-between p-3 rounded border gap-2 ${
|
||||
isStudying ? 'border-purple-500 bg-purple-900/20' :
|
||||
milestoneInfo ? 'border-amber-500/50 bg-amber-900/10' :
|
||||
'border-gray-700 bg-gray-800/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold text-sm">{skillDisplayName}</span>
|
||||
{level > 0 && <span className="text-purple-400 text-sm">Lv.{level}</span>}
|
||||
{selectedUpgrades.length > 0 && (
|
||||
<div className="flex gap-1">
|
||||
{selectedL5.length > 0 && (
|
||||
<Badge className="bg-amber-700/50 text-amber-200 text-xs">L5: {selectedL5.length}</Badge>
|
||||
)}
|
||||
{selectedL10.length > 0 && (
|
||||
<Badge className="bg-purple-700/50 text-purple-200 text-xs">L10: {selectedL10.length}</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 italic">{def.desc}{level > 0 && tierMultiplier !== 1 && ` (Tier ${tierMultiplier}x effect)`}</div>
|
||||
{!prereqMet && def.req && (
|
||||
<div className="text-xs text-red-400 mt-1">
|
||||
Requires: {Object.entries(def.req).map(([r, rl]) => `${r} Lv.${rl}`).join(', ')}
|
||||
</div>
|
||||
)}
|
||||
<SkillMultipliers
|
||||
effectiveStudyTime={effectiveStudyTime}
|
||||
speedMult={speedMult}
|
||||
costMult={costMult}
|
||||
cost={cost}
|
||||
additionalCost={additionalCost}
|
||||
/>
|
||||
|
||||
{hasInsufficientMana && (
|
||||
<div className="text-xs text-red-400 mt-1">
|
||||
Insufficient mana! Need {cost} mana to study.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MilestoneProgress milestoneInfo={milestoneInfo} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 flex-wrap sm:flex-nowrap">
|
||||
{/* Level dots */}
|
||||
<div className="flex gap-1 shrink-0">
|
||||
{Array.from({ length: def.max }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-2 h-2 rounded-full border ${
|
||||
i < level ? 'bg-purple-500 border-purple-400' :
|
||||
i === 4 || i === 9 ? 'border-amber-500' :
|
||||
'border-gray-600'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isStudying ? (
|
||||
<div className="text-xs text-purple-400">
|
||||
{formatStudyTime(currentStudyTarget?.progress || 0)}/{formatStudyTime(def.studyTime * (level > 1 ? level : 1))}
|
||||
</div>
|
||||
) : milestoneInfo ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
onClick={() => onUpgradeDialogOpen(skillId, milestoneInfo.milestone)}
|
||||
>
|
||||
Choose Upgrades
|
||||
</Button>
|
||||
) : canTierUp ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-purple-600 hover:bg-purple-700"
|
||||
onClick={() => onTierUp(skillId)}
|
||||
>
|
||||
⬆️ Tier Up
|
||||
</Button>
|
||||
) : maxed ? (
|
||||
<Badge className="bg-green-900/50 text-green-300">Maxed</Badge>
|
||||
) : (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant={canStudy ? 'default' : 'outline'}
|
||||
disabled={!canStudy}
|
||||
className={canStudy ? 'bg-purple-600 hover:bg-purple-700' : 'opacity-50'}
|
||||
onClick={() => {
|
||||
if (cost > 0) {
|
||||
onStudy(skillId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Study ({cost}{additionalCost && additionalCost.type === 'element' && ` + ${additionalCost.amount} ${ELEMENTS[additionalCost.element]?.sym}`}
|
||||
</Button>
|
||||
{hasSpecial(upgradeEffects, SPECIAL_EFFECTS.PARALLEL_STUDY) &&
|
||||
currentStudyTarget &&
|
||||
!isParallelStudy &&
|
||||
canParallelStudy &&
|
||||
canStudy && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-cyan-500 text-cyan-400 hover:bg-cyan-900/30"
|
||||
onClick={() => {
|
||||
if (cost > 0) {
|
||||
onParallelStudy(skillId);
|
||||
}
|
||||
}}
|
||||
>
|
||||
⚡
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Study in parallel (50% speed)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useCombatStore, useCraftingStore, useManaStore, usePrestigeStore } from '@/lib/game/stores';
|
||||
import { GameCard, ElementBadge } from '@/components/ui';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects';
|
||||
import { canAffordSpellCost } from '@/lib/game/utils';
|
||||
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
||||
|
||||
export function SpellsTab() {
|
||||
// Use modular stores directly
|
||||
const spells = useCombatStore((s) => s.spells);
|
||||
const activeSpell = useCombatStore((s) => s.activeSpell);
|
||||
const setSpell = useCombatStore((s) => s.setSpell);
|
||||
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const unlockedEffects = useCraftingStore((s) => s.unlockedEffects);
|
||||
|
||||
// Get spells from equipment
|
||||
const equipmentSpellIds: string[] = [];
|
||||
const spellSources: Record<string, string[]> = {};
|
||||
|
||||
// Guard against undefined stores during initialization
|
||||
if (!equippedInstances || !equipmentInstances) {
|
||||
return (
|
||||
|
||||
<DebugName name="SpellsTab">
|
||||
<div className="p-4 text-center text-[var(--text-muted)]">
|
||||
Loading spell data...
|
||||
</div>
|
||||
|
||||
</DebugName>);
|
||||
}
|
||||
|
||||
for (const instanceId of Object.values(equippedInstances || {})) {
|
||||
if (!instanceId) continue;
|
||||
const instance = equipmentInstances[instanceId];
|
||||
if (!instance) continue;
|
||||
|
||||
for (const ench of instance.enchantments) {
|
||||
const effectDef = ENCHANTMENT_EFFECTS?.[ench.effectId];
|
||||
if (effectDef?.effect.type === 'spell' && effectDef.effect.spellId) {
|
||||
const spellId = effectDef.effect.spellId;
|
||||
if (!equipmentSpellIds.includes(spellId)) {
|
||||
equipmentSpellIds.push(spellId);
|
||||
}
|
||||
if (!spellSources[spellId]) {
|
||||
spellSources[spellId] = [];
|
||||
}
|
||||
spellSources[spellId].push(instance.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const canCastSpell = (spellId: string): boolean => {
|
||||
const spell = SPELLS_DEF[spellId];
|
||||
if (!spell || !spell.cost) return false;
|
||||
return canAffordSpellCost(spell.cost, rawMana, elements);
|
||||
};
|
||||
|
||||
const hasPactSpells = signedPacts && signedPacts.length > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Equipment-Granted Spells */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--mana-crystal)]">
|
||||
Known Spells
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||
Spells are obtained by enchanting equipment with spell effects.
|
||||
Visit the Crafting tab to design and apply enchantments.
|
||||
</p>
|
||||
|
||||
{equipmentSpellIds.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{equipmentSpellIds.map(id => {
|
||||
const def = SPELLS_DEF[id];
|
||||
if (!def) return null;
|
||||
|
||||
const isActive = activeSpell === id;
|
||||
const canCast = canCastSpell(id);
|
||||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
||||
const sources = spellSources[id] || [];
|
||||
|
||||
return (
|
||||
<GameCard
|
||||
key={id}
|
||||
className={canCast ? 'ring-1 ring-[var(--color-success)]/30' : ''}
|
||||
>
|
||||
<div className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: def.elem === 'raw' ? 'var(--mana-transfer)' : `var(--mana-${def.elem})` }}
|
||||
>
|
||||
{def.name}
|
||||
</h4>
|
||||
<Badge className="bg-[var(--bg-elevated)] text-[var(--mana-crystal)] text-xs border border-[var(--mana-crystal)]/30">
|
||||
Equipment
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-[var(--text-secondary)]">
|
||||
{def.elem !== 'raw' && (
|
||||
<span className="mr-2">
|
||||
<ElementBadge element={def.elem} size="sm" /> {elemDef?.name}
|
||||
</span>
|
||||
)}
|
||||
<span>⚔️ {def.dmg} dmg</span>
|
||||
</div>
|
||||
<div className="text-xs font-[var(--font-mono)]" style={{ color: getSpellCostColor(def.cost) }}>
|
||||
Cost: {formatSpellCost(def.cost)}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--mana-crystal)]/70">From: {sources.join(', ')}</div>
|
||||
<div className="flex gap-2">
|
||||
{isActive ? (
|
||||
<Badge className="bg-[var(--bg-elevated)] text-[var(--color-warning)] border border-[var(--color-warning)]/30">
|
||||
Active
|
||||
</Badge>
|
||||
) : (
|
||||
<button
|
||||
className="px-3 py-1 text-xs border border-[var(--border-default)] rounded hover:border-[var(--border-focus)] transition-colors"
|
||||
onClick={() => setSpell(id)}
|
||||
>
|
||||
Set Active
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</GameCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center p-8 bg-[var(--bg-sunken)] rounded border border-[var(--border-subtle)]">
|
||||
<div className="text-[var(--text-muted)] mb-2">No spells known yet</div>
|
||||
<div className="text-sm text-[var(--text-muted)]">Enchant a staff with a spell effect to gain spells</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pact Spells (from guardian defeats) - Empty State */}
|
||||
{!hasPactSpells && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--color-warning)]">
|
||||
Pact Spells
|
||||
</h3>
|
||||
<div className="text-center p-6 bg-[var(--bg-sunken)] rounded border border-[var(--border-subtle)]">
|
||||
<p className="text-sm text-[var(--text-muted)]">Defeat guardians and sign pacts to unlock powerful spells</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasPactSpells && (
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--color-warning)]">
|
||||
Pact Spells
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-3">Spells earned through guardian pacts appear here.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Spell Reference - show all available spells for enchanting */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-[var(--font-heading)] font-semibold mb-3 text-[var(--mana-death)]">
|
||||
Spell Reference
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||
These spells can be applied to equipment through the enchanting system.
|
||||
Research enchantment effects in the Skills tab to unlock them for designing.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Object.entries(SPELLS_DEF).map(([id, def]) => {
|
||||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
||||
const isUnlocked = unlockedEffects?.includes(`spell_${id}`);
|
||||
|
||||
return (
|
||||
<GameCard
|
||||
key={id}
|
||||
variant={isUnlocked ? "default" : "sunken"}
|
||||
className={isUnlocked ? 'border-[var(--mana-death)]/50' : 'opacity-60'}
|
||||
>
|
||||
<div className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4
|
||||
className="text-sm font-semibold"
|
||||
style={{ color: def.elem === 'raw' ? 'var(--mana-transfer)' : `var(--mana-${def.elem})` }}
|
||||
>
|
||||
{def.name}
|
||||
</h4>
|
||||
<div className="flex gap-1">
|
||||
{def.tier > 0 && (
|
||||
<span className="text-xs px-1.5 py-0.5 border border-[var(--border-default)] rounded">
|
||||
T{def.tier}
|
||||
</span>
|
||||
)}
|
||||
{isUnlocked && (
|
||||
<Badge className="bg-[var(--bg-elevated)] text-[var(--mana-death)] text-xs border border-[var(--mana-death)]/30">
|
||||
Unlocked
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-[var(--text-secondary)]">
|
||||
{def.elem !== 'raw' && (
|
||||
<span className="mr-2">
|
||||
<ElementBadge element={def.elem} size="sm" /> {elemDef?.name}
|
||||
</span>
|
||||
)}
|
||||
<span>⚔️ {def.dmg} dmg</span>
|
||||
</div>
|
||||
<div className="text-xs font-[var(--font-mono)]" style={{ color: getSpellCostColor(def.cost) }}>
|
||||
Cost: {formatSpellCost(def.cost)}
|
||||
</div>
|
||||
{def.desc && (
|
||||
<div className="text-xs text-[var(--text-muted)] italic">{def.desc}</div>
|
||||
)}
|
||||
{!isUnlocked && (
|
||||
<div className="text-xs text-[var(--color-warning)]/70">Research to unlock for enchanting</div>
|
||||
)}
|
||||
</div>
|
||||
</GameCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SpellsTab.displayName = "SpellsTab";
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
@@ -1,100 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ELEMENTS, SPELLS_DEF } from '@/lib/game/constants';
|
||||
import { calcDamage } from '@/lib/game/stores';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
||||
import { canAffordSpellCost } from '@/lib/game/stores';
|
||||
import type { EquipmentSpellState } from '@/lib/game/types';
|
||||
|
||||
interface ActiveSpellsProps {
|
||||
activeEquipmentSpells: { spellId: string; equipmentId: string }[];
|
||||
spells: Record<string, { learned: boolean; level: number; studyProgress: number }>;
|
||||
equipmentSpellStates: EquipmentSpellState[];
|
||||
skills: Record<string, number>;
|
||||
skillUpgrades: Record<string, string[]>;
|
||||
skillTiers: Record<string, number>;
|
||||
signedPacts: number[];
|
||||
currentAction: string;
|
||||
floorElem: string;
|
||||
canCastSpell: (spellId: string) => boolean;
|
||||
}
|
||||
|
||||
export function SpireActiveSpells({
|
||||
activeEquipmentSpells,
|
||||
spells,
|
||||
equipmentSpellStates,
|
||||
skills,
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
signedPacts,
|
||||
currentAction,
|
||||
floorElem,
|
||||
canCastSpell,
|
||||
}: ActiveSpellsProps) {
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
|
||||
<span>Active Spells ({activeEquipmentSpells.length})</span>
|
||||
</div>
|
||||
{activeEquipmentSpells.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
||||
const spellDef = SPELLS_DEF[spellId];
|
||||
if (!spellDef) return null;
|
||||
const spellState = equipmentSpellStates?.find(
|
||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
||||
);
|
||||
const progress = spellState?.castProgress || 0;
|
||||
const canCast = canCastSpell(spellId);
|
||||
|
||||
// Compute progress bar JSX
|
||||
let progressBar = null;
|
||||
if (currentAction === 'climb') {
|
||||
const widthPercent = Math.min(100, progress * 100);
|
||||
const elemColor = spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color || '#60A5FA';
|
||||
const backgroundGradient = 'linear-gradient(90deg, ' + elemColor + '99, ' + elemColor + ')';
|
||||
progressBar = (
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>Cast</span>
|
||||
<span>{widthPercent.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{ width: widthPercent + '%', background: backgroundGradient }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={`${spellId}-${equipmentId}`} className="p-2 rounded bg-gray-800/30 border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
||||
{spellDef.name}
|
||||
{spellDef.tier === 0 && <span className="ml-2 bg-gray-600 text-gray-200 text-xs px-1 rounded">Basic</span>}
|
||||
</span>
|
||||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>{canCast ? '✓' : '✗'}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mb-1">
|
||||
⚔️ {calcDamage({ skills, signedPacts, skillUpgrades, skillTiers }, spellId, floorElem)} dmg • <span style={{ color: getSpellCostColor(spellDef.cost) }}>{formatSpellCost(spellDef.cost)}</span> {' '}• ⚡ {Math.floor(calcDamage({ skills, signedPacts, skillUpgrades, skillTiers }, spellId, floorElem) * (spellDef.castSpeed || 1))} dmg/hr
|
||||
</div>
|
||||
{progressBar}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 text-sm">No spells on equipped weapons. Enchant a staff with spell effects in the Crafting tab.</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Mountain } from 'lucide-react';
|
||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
|
||||
interface SpireGolemsProps {
|
||||
golemancy: {
|
||||
summonedGolems: string[];
|
||||
enabledGolems: string[];
|
||||
};
|
||||
skills: Record<string, number>;
|
||||
floorElem: string;
|
||||
currentAction: string;
|
||||
}
|
||||
|
||||
export function SpireGolems({ golemancy, skills, floorElem }: SpireGolemsProps) {
|
||||
if (golemancy.summonedGolems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-amber-600/50">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
|
||||
<Mountain className="w-4 h-4" />
|
||||
Active Golems ({golemancy.summonedGolems.length})
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{golemancy.summonedGolems.map((summoned) => {
|
||||
const golemDef = GOLEMS_DEF[summoned.golemId];
|
||||
if (!golemDef) return null;
|
||||
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
||||
const damage = getGolemDamage(summoned.golemId, skills);
|
||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, skills);
|
||||
|
||||
return (
|
||||
<div key={summoned.golemId} className="p-2 rounded bg-gray-800/30 border border-gray-700">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Mountain className="w-3 h-3" style={{ color: elemColor }} />
|
||||
<span className="text-xs font-semibold" style={{ color: elemColor }}>{golemDef.name}</span>
|
||||
</div>
|
||||
{golemDef.isAoe && <span className="text-xs border border-gray-600 px-1 rounded">AOE {golemDef.aoeTargets}</span>}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr</div>
|
||||
{currentAction === 'climb' && summoned.attackProgress > 0 && (
|
||||
<div className="mt-1">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-0.5">
|
||||
<span>Attack</span>
|
||||
<span>{(summoned.attackProgress * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-1 bg-gray-700 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.min(100, summoned.attackProgress * 100)}%`, background: elemColor }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Mountain } from 'lucide-react';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
|
||||
interface SpireHeaderProps {
|
||||
currentFloor: number;
|
||||
maxFloorReached: number;
|
||||
signedPacts: number;
|
||||
isGuardianFloor: boolean;
|
||||
roomType: string;
|
||||
roomLabel: string;
|
||||
roomIcon: string;
|
||||
roomColor: string;
|
||||
floorElem: string;
|
||||
floorElemDef: any;
|
||||
simpleMode: boolean;
|
||||
}
|
||||
|
||||
export function SpireHeader({
|
||||
currentFloor,
|
||||
maxFloorReached,
|
||||
signedPacts,
|
||||
isGuardianFloor,
|
||||
roomType,
|
||||
roomLabel,
|
||||
roomIcon,
|
||||
roomColor,
|
||||
floorElem,
|
||||
floorElemDef,
|
||||
simpleMode,
|
||||
}: SpireHeaderProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Current Floor Card - Only show in Spire Mode (simpleMode) */}
|
||||
{simpleMode && (
|
||||
<div className="bg-gray-900/80 border-gray-700 rounded-lg p-4 lg:p-6">
|
||||
<div className="pb-2 mb-4 border-b border-gray-700">
|
||||
<h3 className="text-amber-400 text-xs font-semibold tracking-wide uppercase flex items-center justify-between">
|
||||
<span>Current Floor</span>
|
||||
<Badge
|
||||
className="ml-2"
|
||||
style={{
|
||||
backgroundColor: `${roomColor}20`,
|
||||
color: roomColor,
|
||||
borderColor: `${roomColor}60`
|
||||
}}
|
||||
>
|
||||
{roomIcon} {roomLabel}
|
||||
</Badge>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-4xl font-bold tracking-tight" style={{ color: floorElemDef?.color }}>
|
||||
{currentFloor}
|
||||
</span>
|
||||
<span className="text-gray-400 text-sm">/ 100</span>
|
||||
<span className="ml-auto text-sm" style={{ color: floorElemDef?.color }}>
|
||||
{floorElemDef?.sym} {floorElemDef?.name}
|
||||
</span>
|
||||
{isGuardianFloor && (
|
||||
<Badge className="bg-red-900/50 text-red-300 border-red-600">GUARDIAN</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isGuardianFloor && (
|
||||
<div className="text-sm font-semibold" style={{ color: floorElemDef?.color }}>
|
||||
⚔️ Guardian Floor
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="text-sm text-gray-400">
|
||||
Best: Floor <strong className="text-gray-200">{maxFloorReached}</strong> •
|
||||
Pacts: <strong className="text-amber-400">{signedPacts}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Spire Stats Card - Only show in normal mode */}
|
||||
{!simpleMode && (
|
||||
<div className="bg-gray-900/80 border-gray-700 rounded-lg p-4 lg:p-6">
|
||||
<div className="pb-2 mb-4 border-b border-gray-700">
|
||||
<h3 className="text-amber-400 text-xs font-semibold tracking-wide uppercase">Spire Stats</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-amber-400 tracking-tight">{maxFloorReached}</div>
|
||||
<div className="text-xs text-gray-400">Best Floor</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded">
|
||||
<div className="text-2xl font-bold text-purple-400 tracking-tight">{signedPacts}</div>
|
||||
<div className="text-xs text-gray-400">Pacts Signed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-sm text-gray-400">
|
||||
Current Floor: <strong className="text-gray-200">{currentFloor}</strong>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Mountain, ChevronDown } from 'lucide-react';
|
||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { calcDamage, getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/utils';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
||||
import { canAffordSpellCost, getFloorElement } from '@/lib/game/utils';
|
||||
import { useManaStore, useSkillStore, useCombatStore, usePrestigeStore, useCraftingStore } from '@/lib/game/stores';
|
||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
||||
|
||||
// Extracted components
|
||||
import { SpireHeader } from './SpireHeader';
|
||||
import { GuardianPanel } from './GuardianPanel';
|
||||
import { RoomDisplay } from './RoomDisplay';
|
||||
import { FloorControls } from './FloorControls';
|
||||
import { CombatStatsPanel } from './CombatStatsPanel';
|
||||
import { ActivityLog } from './ActivityLog';
|
||||
import { SpireActiveSpells } from './SpireActiveSpells';
|
||||
import { SpireGolems } from './SpireGolems';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
|
||||
// Room type configurations
|
||||
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
|
||||
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
|
||||
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
|
||||
speed: { label: 'Speed', icon: '💨', color: '#3B82F6' },
|
||||
guardian: { label: 'Guardian', icon: '🛡️', color: '#EF4444' },
|
||||
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
|
||||
};
|
||||
|
||||
interface SpireTabProps {
|
||||
simpleMode?: boolean;
|
||||
}
|
||||
|
||||
// Check if player can enter spire mode
|
||||
const canEnterSpireMode = (spireMode: boolean): boolean => {
|
||||
return !spireMode;
|
||||
};
|
||||
|
||||
export function SpireTab({ simpleMode = false }: SpireTabProps) {
|
||||
// Get state from modular stores
|
||||
const currentFloor = useCombatStore((s) => s.currentFloor);
|
||||
const floorHP = useCombatStore((s) => s.floorHP);
|
||||
const floorMaxHP = useCombatStore((s) => s.floorMaxHP);
|
||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
||||
const currentAction = useCombatStore((s) => s.currentAction);
|
||||
const castProgress = useCombatStore((s) => s.castProgress);
|
||||
const activeSpell = useCombatStore((s) => s.activeSpell);
|
||||
const spireMode = useCombatStore((s) => s.spireMode);
|
||||
const climbDirection = useCombatStore((s) => s.climbDirection) || 'up';
|
||||
const clearedFloors = useCombatStore((s) => s.clearedFloors || {});
|
||||
const currentRoom = useCombatStore((s) => s.currentRoom);
|
||||
const equipmentSpellStates = useCombatStore((s) => s.equipmentSpellStates);
|
||||
const golemancy = useCombatStore((s) => s.golemancy);
|
||||
const activityLog = useCombatStore((s) => s.activityLog);
|
||||
|
||||
const currentStudyTarget = useSkillStore((s) => s.currentStudyTarget);
|
||||
const parallelStudyTarget = useSkillStore((s) => s.parallelStudyTarget);
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
|
||||
const designProgress = useCraftingStore((s) => s.designProgress);
|
||||
const designProgress2 = useCraftingStore((s) => s.designProgress2);
|
||||
const preparationProgress = useCraftingStore((s) => s.preparationProgress);
|
||||
const applicationProgress = useCraftingStore((s) => s.applicationProgress);
|
||||
const equipmentCraftingProgress = useCraftingStore((s) => s.equipmentCraftingProgress);
|
||||
|
||||
// Derived data
|
||||
const floorElem = getFloorElement(currentFloor);
|
||||
const floorElemDef = ELEMENTS[floorElem];
|
||||
const isGuardianFloor = !!GUARDIANS[currentFloor];
|
||||
const currentGuardian = GUARDIANS[currentFloor];
|
||||
const isFloorCleared = clearedFloors[currentFloor];
|
||||
const roomType = currentRoom?.roomType || 'combat';
|
||||
const roomConfig = ROOM_TYPE_CONFIG[roomType] || ROOM_TYPE_CONFIG.combat;
|
||||
|
||||
const activeEquipmentSpells = useMemo(
|
||||
() => getActiveEquipmentSpells(equippedInstances, equipmentInstances),
|
||||
[equippedInstances, equipmentInstances]
|
||||
);
|
||||
|
||||
const upgradeEffects = useMemo(
|
||||
() => getUnifiedEffects({
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
equippedInstances,
|
||||
equipmentInstances,
|
||||
}),
|
||||
[skillUpgrades, skillTiers, equippedInstances, equipmentInstances]
|
||||
);
|
||||
|
||||
const totalDPS = useMemo(
|
||||
() => getTotalDPS({ skills, signedPacts, skillUpgrades, skillTiers }, upgradeEffects, floorElem),
|
||||
[skills, signedPacts, skillUpgrades, skillTiers, upgradeEffects, floorElem]
|
||||
);
|
||||
|
||||
// Spell casting check
|
||||
const canCastSpell = (spellId: string): boolean => {
|
||||
const spell = SPELLS_DEF[spellId];
|
||||
if (!spell || !spell.cost) return false;
|
||||
return canAffordSpellCost(spell.cost, rawMana, elements);
|
||||
};
|
||||
|
||||
// Climb handlers - defined OUTSIDE of JSX
|
||||
const handleClimbUp = () => {
|
||||
useCombatStore.getState().startClimbUp();
|
||||
};
|
||||
|
||||
const handleClimbDown = () => {
|
||||
useCombatStore.getState().startClimbDown();
|
||||
};
|
||||
|
||||
const handleClimb = (direction: 'up' | 'down') => {
|
||||
if (direction === 'up') {
|
||||
handleClimbUp();
|
||||
} else {
|
||||
handleClimbDown();
|
||||
}
|
||||
};
|
||||
|
||||
const getSkillName = (skillId: string): string => {
|
||||
return SKILLS_DEF[skillId]?.name || skillId;
|
||||
};
|
||||
|
||||
// Handle exit spire mode
|
||||
const exitSpireMode = () => {
|
||||
useCombatStore.getState().exitSpireMode();
|
||||
};
|
||||
|
||||
// Handle enter spire mode
|
||||
const enterSpireMode = () => {
|
||||
useCombatStore.getState().enterSpireMode();
|
||||
};
|
||||
|
||||
return (
|
||||
<DebugName name="SpireTab">
|
||||
<div className="grid gap-4">
|
||||
{/* Enter Spire Mode - Normal mode only */}
|
||||
{!simpleMode && (
|
||||
<Card className="bg-gray-900/80 border-amber-600/50">
|
||||
<CardContent className="pt-4">
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700"
|
||||
size="lg"
|
||||
onClick={enterSpireMode}
|
||||
disabled={!canEnterSpireMode(spireMode)}
|
||||
>
|
||||
<Mountain className="w-5 h-5 mr-2" />
|
||||
Enter Spire Mode
|
||||
</Button>
|
||||
<div className="text-xs text-gray-400 text-center mt-2">
|
||||
Climb the Spire to face guardians and earn pacts
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Exit Spire Mode - Spire mode only */}
|
||||
{simpleMode && (
|
||||
<Card className="bg-gray-900/80 border-red-600/50">
|
||||
<CardContent className="pt-4">
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800"
|
||||
size="lg"
|
||||
onClick={exitSpireMode}
|
||||
>
|
||||
<ChevronDown className="w-5 h-5 mr-2" />
|
||||
Exit Spire Mode
|
||||
</Button>
|
||||
<div className="text-xs text-gray-400 text-center mt-2">
|
||||
Climb down to floor 1 to return to the main game
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Spire Header */}
|
||||
<SpireHeader
|
||||
currentFloor={currentFloor}
|
||||
maxFloorReached={maxFloorReached}
|
||||
signedPacts={signedPacts.length}
|
||||
isGuardianFloor={isGuardianFloor}
|
||||
roomType={roomType}
|
||||
roomLabel={roomConfig.label}
|
||||
roomIcon={roomConfig.icon}
|
||||
roomColor={roomConfig.color}
|
||||
floorElem={floorElem}
|
||||
floorElemDef={floorElemDef}
|
||||
simpleMode={simpleMode}
|
||||
/>
|
||||
|
||||
{/* Active Spells Card - Spire Mode only */}
|
||||
{simpleMode && (
|
||||
<SpireActiveSpells
|
||||
activeEquipmentSpells={activeEquipmentSpells}
|
||||
spells={useSkillStore.getState().spells}
|
||||
equipmentSpellStates={equipmentSpellStates}
|
||||
skills={skills}
|
||||
skillUpgrades={skillUpgrades}
|
||||
skillTiers={skillTiers}
|
||||
signedPacts={signedPacts}
|
||||
currentAction={currentAction}
|
||||
floorElem={floorElem}
|
||||
canCastSpell={canCastSpell}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Summoned Golems */}
|
||||
{simpleMode && golemancy.summonedGolems.length > 0 && (
|
||||
<SpireGolems
|
||||
golemancy={golemancy}
|
||||
skills={skills}
|
||||
currentAction={currentAction}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Guardian Panel */}
|
||||
{isGuardianFloor && simpleMode && (
|
||||
<GuardianPanel
|
||||
currentFloor={currentFloor}
|
||||
floorElemDef={floorElemDef}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Room Display */}
|
||||
{simpleMode && (
|
||||
<RoomDisplay
|
||||
roomType={roomType}
|
||||
roomConfig={roomConfig}
|
||||
primaryEnemy={currentRoom?.enemies?.[0] || null}
|
||||
swarmEnemies={roomType === 'swarm' ? (currentRoom?.enemies || []) : []}
|
||||
puzzleId={currentRoom?.puzzleId}
|
||||
puzzleProgress={currentRoom?.puzzleProgress}
|
||||
simpleMode={true}
|
||||
floorElemDef={floorElemDef}
|
||||
floorHP={floorHP}
|
||||
floorMaxHP={floorMaxHP}
|
||||
totalDPS={totalDPS}
|
||||
currentAction={currentAction}
|
||||
activeEquipmentSpells={activeEquipmentSpells}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Floor Controls */}
|
||||
{simpleMode && (
|
||||
<FloorControls
|
||||
currentFloor={currentFloor}
|
||||
floorHP={floorHP}
|
||||
floorMaxHP={floorMaxHP}
|
||||
maxFloorReached={maxFloorReached}
|
||||
equipmentSpellStates={equipmentSpellStates}
|
||||
skills={skills}
|
||||
signedPacts={signedPacts}
|
||||
storeCurrentAction={currentAction}
|
||||
climbDirection={climbDirection}
|
||||
isGuardianFloor={isGuardianFloor}
|
||||
currentRoom={currentRoom}
|
||||
currentGuardian={currentGuardian}
|
||||
isFloorCleared={isFloorCleared}
|
||||
floorElemDef={floorElemDef}
|
||||
roomType={roomType}
|
||||
roomConfig={roomConfig}
|
||||
activeEquipmentSpells={activeEquipmentSpells}
|
||||
floorElem={floorElem}
|
||||
totalDPS={totalDPS}
|
||||
calcDamage={calcDamage}
|
||||
SPELLS_DEF={SPELLS_DEF}
|
||||
canCastSpell={canCastSpell}
|
||||
handleClimb={handleClimb}
|
||||
formatSpellCost={formatSpellCost}
|
||||
getSpellCostColor={getSpellCostColor}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Combat Stats Panel */}
|
||||
{simpleMode && (
|
||||
<CombatStatsPanel
|
||||
activeEquipmentSpells={activeEquipmentSpells}
|
||||
storeCurrentAction={currentAction}
|
||||
totalDPS={totalDPS}
|
||||
calcDamage={calcDamage}
|
||||
formatSpellCost={formatSpellCost}
|
||||
getSpellCostColor={getSpellCostColor}
|
||||
SPELLS_DEF={SPELLS_DEF}
|
||||
upgradeEffects={upgradeEffects}
|
||||
canCastSpell={canCastSpell}
|
||||
studySpeedMult={1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Activity Log - Spire Mode only */}
|
||||
{simpleMode && <ActivityLog activityLog={activityLog} />}
|
||||
|
||||
{/* Study Progress - Normal mode only */}
|
||||
{!simpleMode && currentStudyTarget && (
|
||||
<Card className="bg-gray-900/80 border-purple-600/50">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
<div className="text-xs text-gray-400 mb-2">Study: {getSkillName(currentStudyTarget.id)}</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300 bg-purple-500"
|
||||
style={{ width: `${Math.min(100, (currentStudyTarget.progress / currentStudyTarget.required) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
{parallelStudyTarget && (
|
||||
<div className="mt-3 p-2 rounded border border-cyan-600/50 bg-cyan-900/20">
|
||||
<div className="text-xs text-cyan-300 mb-1">Parallel: {getSkillName(parallelStudyTarget.id)} (50% speed)</div>
|
||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300 bg-cyan-500"
|
||||
style={{ width: `${Math.min(100, (parallelStudyTarget.progress / parallelStudyTarget.required) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Crafting Progress - Normal mode only */}
|
||||
{!simpleMode && (designProgress || preparationProgress || applicationProgress) && (
|
||||
<Card className="bg-gray-900/80 border-cyan-600/50">
|
||||
<CardContent className="pt-4 pb-4">
|
||||
{designProgress && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs text-gray-400 mb-1">Design Progress</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300 bg-cyan-500"
|
||||
style={{ width: `${Math.min(100, (designProgress.progress / designProgress.required) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{preparationProgress && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs text-gray-400 mb-1">Preparation Progress</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300 bg-cyan-500"
|
||||
style={{ width: `${Math.min(100, (preparationProgress.progress / preparationProgress.required) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{applicationProgress && (
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 mb-1">Application Progress</div>
|
||||
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300 bg-cyan-500"
|
||||
style={{ width: `${Math.min(100, (applicationProgress.progress / applicationProgress.required) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</DebugName>
|
||||
);
|
||||
}
|
||||
|
||||
SpireTab.displayName = "SpireTab";
|
||||
@@ -1,331 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { ELEMENTS, GUARDIANS } from '@/lib/game/constants';
|
||||
import { getTierMultiplier } from '@/lib/game/skill-evolution';
|
||||
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
import { fmt, fmtDec, computeMaxMana, computeRegen, computeClickMana, getMeditationBonus, getIncursionStrength, getStudySpeedMultiplier, getStudyCostMultiplier } from '@/lib/game/stores';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { FlaskConical, Trophy, RotateCcw } from 'lucide-react';
|
||||
import { ManaStatsSection } from '../stats/ManaStatsSection';
|
||||
import { ManaTypeBreakdown } from '../stats/ManaTypeBreakdown';
|
||||
import { CombatStatsSection } from '../stats/CombatStatsSection';
|
||||
import { StudyStatsSection } from '../stats/StudyStatsSection';
|
||||
import { UpgradeEffectsSection } from '../stats/UpgradeEffectsSection';
|
||||
|
||||
// Modular stores
|
||||
import { useCombatStore, useManaStore, useSkillStore, usePrestigeStore, useGameStore } from '@/lib/game/stores';
|
||||
import { useCraftingStore } from '@/lib/game/stores/craftingStore';
|
||||
import { DebugName } from '@/lib/game/debug-context';
|
||||
|
||||
export function StatsTab() {
|
||||
// Get state from modular stores
|
||||
const skills = useSkillStore((s) => s.skills);
|
||||
const skillTiers = useSkillStore((s) => s.skillTiers);
|
||||
const skillUpgrades = useSkillStore((s) => s.skillUpgrades);
|
||||
|
||||
const prestigeUpgrades = usePrestigeStore((s) => s.prestigeUpgrades);
|
||||
const loopCount = usePrestigeStore((s) => s.loopCount);
|
||||
const insight = usePrestigeStore((s) => s.insight);
|
||||
const totalInsight = usePrestigeStore((s) => s.totalInsight);
|
||||
const memorySlots = usePrestigeStore((s) => s.memorySlots);
|
||||
const signedPacts = usePrestigeStore((s) => s.signedPacts);
|
||||
|
||||
const elements = useManaStore((s) => s.elements);
|
||||
const totalManaGathered = useManaStore((s) => s.totalManaGathered);
|
||||
const rawMana = useManaStore((s) => s.rawMana);
|
||||
const meditateTicks = useManaStore((s) => s.meditateTicks);
|
||||
|
||||
const maxFloorReached = useCombatStore((s) => s.maxFloorReached);
|
||||
const spells = useCombatStore((s) => s.spells);
|
||||
|
||||
// Get equipment state from crafting store
|
||||
const equippedInstances = useCraftingStore(s => s.equippedInstances);
|
||||
const equipmentInstances = useCraftingStore(s => s.equipmentInstances);
|
||||
|
||||
// Compute unified effects
|
||||
const upgradeEffects = getUnifiedEffects({
|
||||
skillUpgrades,
|
||||
skillTiers,
|
||||
equippedInstances,
|
||||
equipmentInstances
|
||||
});
|
||||
|
||||
// Compute derived stats
|
||||
const maxMana = computeMaxMana({
|
||||
skills,
|
||||
prestigeUpgrades,
|
||||
skillUpgrades,
|
||||
skillTiers
|
||||
}, upgradeEffects);
|
||||
|
||||
const baseRegen = computeRegen({
|
||||
skills,
|
||||
prestigeUpgrades,
|
||||
skillUpgrades,
|
||||
skillTiers
|
||||
}, upgradeEffects);
|
||||
|
||||
const clickMana = computeClickMana({
|
||||
skills,
|
||||
prestigeUpgrades,
|
||||
skillUpgrades,
|
||||
skillTiers
|
||||
});
|
||||
|
||||
const meditationMultiplier = getMeditationBonus(meditateTicks, skills, upgradeEffects.meditationEfficiency);
|
||||
|
||||
const day = useGameStore((s) => s.day);
|
||||
const hour = useGameStore((s) => s.hour);
|
||||
const incursionStrength = getIncursionStrength(day, hour);
|
||||
|
||||
// Effective regen with incursion penalty
|
||||
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
|
||||
|
||||
// Mana Cascade bonus
|
||||
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
|
||||
? Math.floor(maxMana / 100) * 0.1
|
||||
: 0;
|
||||
|
||||
// Mana Waterfall bonus
|
||||
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
|
||||
? Math.floor(maxMana / 100) * 0.25
|
||||
: 0;
|
||||
|
||||
// Effective regen
|
||||
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
|
||||
|
||||
// Get study speed/cost multipliers
|
||||
const studySpeedMult = getStudySpeedMultiplier(skills);
|
||||
const studyCostMult = getStudyCostMultiplier(skills);
|
||||
|
||||
// Check special effects
|
||||
const hasManaWaterfall = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL);
|
||||
const hasFlowSurge = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.FLOW_SURGE);
|
||||
const hasManaOverflow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_OVERFLOW);
|
||||
const hasEternalFlow = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.ETERNAL_FLOW);
|
||||
|
||||
// Compute element max
|
||||
const elemMax = (() => {
|
||||
const ea = skillTiers?.elemAttune || 1;
|
||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return 10 + level * 50 * tierMult + (prestigeUpgrades.elementalAttune || 0) * 25;
|
||||
})();
|
||||
|
||||
return (
|
||||
<DebugName name="StatsTab">
|
||||
<div className="space-y-4">
|
||||
{/* Mana Stats */}
|
||||
<ManaStatsSection
|
||||
upgradeEffects={upgradeEffects}
|
||||
maxMana={maxMana}
|
||||
baseRegen={baseRegen}
|
||||
clickMana={clickMana}
|
||||
meditationMultiplier={meditationMultiplier}
|
||||
effectiveRegen={effectiveRegen}
|
||||
incursionStrength={incursionStrength}
|
||||
manaCascadeBonus={manaCascadeBonus}
|
||||
manaWaterfallBonus={manaWaterfallBonus}
|
||||
hasManaWaterfall={hasManaWaterfall}
|
||||
hasFlowSurge={hasFlowSurge}
|
||||
hasManaOverflow={hasManaOverflow}
|
||||
hasEternalFlow={hasEternalFlow}
|
||||
/>
|
||||
|
||||
{/* Mana Type Breakdown */}
|
||||
<ManaTypeBreakdown />
|
||||
|
||||
{/* Combat Stats */}
|
||||
<CombatStatsSection />
|
||||
|
||||
{/* Study Stats */}
|
||||
<StudyStatsSection
|
||||
studySpeedMult={studySpeedMult}
|
||||
studyCostMult={studyCostMult}
|
||||
/>
|
||||
|
||||
{/* Element Stats */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-green-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<FlaskConical className="w-4 h-4" />
|
||||
Element Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Element Capacity:</span>
|
||||
<span className="text-green-300">{elemMax}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Elem. Attunement Bonus:</span>
|
||||
<span className="text-green-300">
|
||||
{(() => {
|
||||
const ea = skillTiers?.elemAttune || 1;
|
||||
const tieredSkillId = ea > 1 ? `elemAttune_t${ea}` : 'elemAttune';
|
||||
const level = skills[tieredSkillId] || skills.elemAttune || 0;
|
||||
const tierMult = getTierMultiplier(tieredSkillId);
|
||||
return `+${level * 50 * tierMult}`;
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Prestige Attunement:</span>
|
||||
<span className="text-green-300">+{(prestigeUpgrades.elementalAttune || 0) * 25}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Unlocked Elements:</span>
|
||||
<span className="text-green-300">{Object.values(elements || {}).filter((e: 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 + (skills.elemCrafting || 0) * 0.25, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-gray-700 my-3" />
|
||||
<div className="text-xs text-gray-400 mb-2">Elemental Mana Pools:</div>
|
||||
<div className="grid grid-cols-4 sm:grid-cols-6 md:grid-cols-8 gap-2">
|
||||
{Object.entries(elements)
|
||||
.filter(([, state]) => state.unlocked)
|
||||
.map(([id, state]) => {
|
||||
const def = ELEMENTS[id];
|
||||
return (
|
||||
<div key={id} className="p-2 rounded border border-gray-700 bg-gray-800/50 text-center">
|
||||
<div className="text-lg">{def?.sym}</div>
|
||||
<div className="text-xs text-gray-400">{state.current}/{state.max}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Active Upgrades */}
|
||||
<UpgradeEffectsSection />
|
||||
|
||||
{/* Enchantment Power */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-blue-400 game-panel-title text-xs flex items-center gap-2">
|
||||
✨ Enchantment Power
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-400">Enchantment Power:</span>
|
||||
<span className="text-blue-300 font-[var(--font-mono)]">
|
||||
{upgradeEffects?.enchantmentPowerMultiplier?.toFixed(2) || '1.0'}×
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Increases the power of all enchantments by {((upgradeEffects?.enchantmentPowerMultiplier || 1) - 1) * 100}%. Multiplier applied to all enchantment effects.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Pact Bonuses */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<Trophy className="w-4 h-4" />
|
||||
Signed Pacts ({signedPacts.length}/10)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{signedPacts.length === 0 ? (
|
||||
<div className="text-gray-500 text-sm">No pacts signed yet. Defeat guardians to earn pacts.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{signedPacts.map((floor) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
if (!guardian) return null;
|
||||
return (
|
||||
<div
|
||||
key={floor}
|
||||
className="flex items-center justify-between p-2 rounded border"
|
||||
style={{ borderColor: guardian.color, backgroundColor: `${guardian.color}15` }}
|
||||
>
|
||||
<div>
|
||||
<div className="font-semibold text-sm" style={{ color: guardian.color }}>
|
||||
{guardian.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">Floor {floor}</div>
|
||||
</div>
|
||||
<Badge className="bg-amber-900/50 text-amber-300">
|
||||
{guardian.pact}x multiplier
|
||||
</Badge>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex justify-between text-sm font-semibold border-t border-gray-700 pt-2 mt-2">
|
||||
<span className="text-gray-300">Combined Pact Multiplier:</span>
|
||||
<span className="text-amber-400">×{fmtDec(signedPacts.reduce((m, f) => m * (GUARDIANS[f]?.pact || 1), 1), 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Loop Stats */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-purple-400 game-panel-title text-xs flex items-center gap-2">
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Loop Stats
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-amber-400 game-mono">{loopCount}</div>
|
||||
<div className="text-xs text-gray-400">Loops Completed</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-purple-400 game-mono">{fmt(insight)}</div>
|
||||
<div className="text-xs text-gray-400">Current Insight</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-blue-400 game-mono">{fmt(totalInsight)}</div>
|
||||
<div className="text-xs text-gray-400">Total Insight</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-2xl font-bold text-green-400 game-mono">{maxFloorReached}</div>
|
||||
<div className="text-xs text-gray-400">Max Floor</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-gray-700 my-3" />
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(spells || {}).filter((s: any) => s.learned).length}</div>
|
||||
<div className="text-xs text-gray-400">Spells Learned</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-gray-300 game-mono">{Object.values(skills).reduce((a, b) => a + b, 0)}</div>
|
||||
<div className="text-xs text-gray-400">Total Skill Levels</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-gray-300 game-mono">{fmt(totalManaGathered)}</div>
|
||||
<div className="text-xs text-gray-400">Total Mana Gathered</div>
|
||||
</div>
|
||||
<div className="p-3 bg-gray-800/50 rounded text-center">
|
||||
<div className="text-xl font-bold text-gray-300 game-mono">{memorySlots}</div>
|
||||
<div className="text-xs text-gray-400">Memory Slots</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</DebugName>
|
||||
);
|
||||
}
|
||||
|
||||
StatsTab.displayName = "StatsTab";
|
||||
@@ -1,75 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import { formatStudyTime } from '@/lib/game/formatting';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import type { StudyTarget } from '@/lib/game/types';
|
||||
|
||||
export interface StudyProgressProps {
|
||||
currentStudyTarget: StudyTarget;
|
||||
skills: Record<string, number>;
|
||||
studySpeedMult: number;
|
||||
cancelStudy: () => void;
|
||||
}
|
||||
|
||||
export function StudyProgress({
|
||||
currentStudyTarget,
|
||||
skills,
|
||||
studySpeedMult,
|
||||
cancelStudy
|
||||
}: StudyProgressProps) {
|
||||
const { id, progress, required } = currentStudyTarget;
|
||||
|
||||
// Get skill name
|
||||
const baseId = id.includes('_t') ? id.split('_t')[0] : id;
|
||||
const skillDef = SKILLS_DEF[baseId];
|
||||
const skillName = skillDef?.name || id;
|
||||
|
||||
// Get current level
|
||||
const currentLevel = skills[id] || skills[baseId] || 0;
|
||||
|
||||
// Calculate progress percentage
|
||||
const progressPercent = Math.min((progress / required) * 100, 100);
|
||||
|
||||
// Estimated completion
|
||||
const remainingHours = required - progress;
|
||||
const effectiveSpeed = studySpeedMult;
|
||||
const realTimeRemaining = remainingHours / effectiveSpeed;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="text-purple-300 font-semibold">{skillName}</span>
|
||||
<span className="text-gray-400 ml-2">
|
||||
Level {currentLevel} → {currentLevel + 1}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={cancelStudy}
|
||||
className="text-xs"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs text-gray-400">
|
||||
<span>{formatStudyTime(progress)} / {formatStudyTime(required)}</span>
|
||||
<span>{progressPercent.toFixed(1)}%</span>
|
||||
</div>
|
||||
<Progress value={progressPercent} className="h-2" />
|
||||
{studySpeedMult > 1 && (
|
||||
<div className="text-xs text-green-400">
|
||||
Speed: {(studySpeedMult * 100).toFixed(0)}% • ETA: {formatStudyTime(realTimeRemaining)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
StudyProgress.displayName = "StudyProgress";
|
||||
@@ -1,117 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { SKILLS_DEF } from '@/lib/game/constants';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import type { SkillUpgradeChoice } from '@/lib/game/types';
|
||||
|
||||
export interface UpgradeDialogProps {
|
||||
open: boolean;
|
||||
skillId: string | null;
|
||||
milestone: 5 | 10;
|
||||
pendingSelections: string[];
|
||||
available: SkillUpgradeChoice[];
|
||||
alreadySelected: string[];
|
||||
onToggle: (upgradeId: string) => void;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function UpgradeDialog({
|
||||
open,
|
||||
skillId,
|
||||
milestone,
|
||||
pendingSelections,
|
||||
available,
|
||||
alreadySelected,
|
||||
onToggle,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onOpenChange,
|
||||
}: UpgradeDialogProps) {
|
||||
if (!skillId) return null;
|
||||
|
||||
// Get skill name
|
||||
const baseId = skillId.includes('_t') ? skillId.split('_t')[0] : skillId;
|
||||
const skillDef = SKILLS_DEF[baseId];
|
||||
const skillName = skillDef?.name || skillId;
|
||||
|
||||
const currentSelections = pendingSelections.length > 0 ? pendingSelections : alreadySelected;
|
||||
const canConfirm = currentSelections.length === 2;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md bg-gray-900 border-purple-600/50">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-amber-400">
|
||||
Level {milestone} Milestone: {skillName}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Choose 2 upgrades for this skill. These choices are permanent.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2 py-4">
|
||||
{available.map((upgrade) => {
|
||||
const isSelected = currentSelections.includes(upgrade.id);
|
||||
const canSelect = isSelected || currentSelections.length < 2;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={upgrade.id}
|
||||
onClick={() => canSelect && onToggle(upgrade.id)}
|
||||
className={`p-3 rounded border cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-amber-500 bg-amber-900/30'
|
||||
: canSelect
|
||||
? 'border-gray-700 hover:border-gray-500 bg-gray-800/30'
|
||||
: 'border-gray-800 bg-gray-900/30 opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className={`font-semibold text-sm ${isSelected ? 'text-amber-300' : 'text-gray-200'}`}>
|
||||
{upgrade.name}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<Badge className="bg-amber-600/50 text-amber-200 text-xs">Selected</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1">{upgrade.desc}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{available.length === 0 && (
|
||||
<div className="text-center text-gray-500 py-4">
|
||||
No upgrades available at this milestone.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex gap-2 sm:gap-0">
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-amber-600 hover:bg-amber-700"
|
||||
disabled={!canConfirm}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Confirm ({currentSelections.length}/2)
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
UpgradeDialog.displayName = "UpgradeDialog";
|
||||
@@ -1,23 +0,0 @@
|
||||
// ─── Tab Components Index ──────────────────────────────────────────────────────
|
||||
// Re-exports all tab components for cleaner imports
|
||||
|
||||
export { CraftingTab } from './CraftingTab';
|
||||
export { SpireTab } from './SpireTab';
|
||||
export { SpellsTab } from './SpellsTab';
|
||||
// SkillsTab is now exported from src/components/game/index.ts
|
||||
export { SkillsTab } from '../SkillsTab';
|
||||
export { StatsTab } from './StatsTab';
|
||||
export { EquipmentTab } from './EquipmentTab';
|
||||
export { AttunementsTab } from './AttunementsTab';
|
||||
export { DebugTab } from './DebugTab';
|
||||
export { LootTab } from './LootTab';
|
||||
export { AchievementsTab } from './AchievementsTab';
|
||||
export { GolemancyTab } from './GolemancyTab';
|
||||
|
||||
// Spire sub-components
|
||||
export { SpireHeader } from './SpireHeader';
|
||||
export { GuardianPanel } from './GuardianPanel';
|
||||
export { RoomDisplay } from './RoomDisplay';
|
||||
export { FloorControls } from './FloorControls';
|
||||
export { CombatStatsPanel } from './CombatStatsPanel';
|
||||
export { ActivityLog } from './ActivityLog';
|
||||
Reference in New Issue
Block a user