269 lines
12 KiB
TypeScript
Executable File
269 lines
12 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getTotalAttunementRegen, getAvailableSkillCategories, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL, getAttunementConversionRate } from '@/lib/game/data/attunements';
|
|
import { ELEMENTS } from '@/lib/game/constants';
|
|
import type { GameStore, AttunementState } from '@/lib/game/types';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Progress } from '@/components/ui/progress';
|
|
import { Lock, Sparkles, TrendingUp } from 'lucide-react';
|
|
|
|
export interface AttunementsTabProps {
|
|
store: GameStore;
|
|
}
|
|
|
|
export function AttunementsTab({ store }: AttunementsTabProps) {
|
|
const attunements = store.attunements || {};
|
|
|
|
// Get active attunements
|
|
const activeAttunements = Object.entries(attunements)
|
|
.filter(([, state]) => state.active)
|
|
.map(([id]) => ATTUNEMENTS_DEF[id])
|
|
.filter(Boolean);
|
|
|
|
// Calculate total regen from attunements
|
|
const totalAttunementRegen = getTotalAttunementRegen(attunements);
|
|
|
|
// Get available skill categories
|
|
const availableCategories = getAvailableSkillCategories(attunements);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Overview Card */}
|
|
<Card className="bg-gray-900/80 border-gray-700">
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-amber-400 text-sm">Your Attunements</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-gray-400 mb-3">
|
|
Attunements are magical bonds tied to specific body locations. Each attunement grants unique capabilities,
|
|
mana regeneration, and access to specialized skills. Level them up to increase their power.
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
<Badge className="bg-teal-900/50 text-teal-300">
|
|
+{totalAttunementRegen.toFixed(1)} raw mana/hr
|
|
</Badge>
|
|
<Badge className="bg-purple-900/50 text-purple-300">
|
|
{activeAttunements.length} active attunement{activeAttunements.length !== 1 ? 's' : ''}
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Attunement Slots */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{Object.entries(ATTUNEMENTS_DEF).map(([id, def]) => {
|
|
const state = attunements[id];
|
|
const isActive = state?.active;
|
|
const isUnlocked = state?.active || def.unlocked;
|
|
const level = state?.level || 1;
|
|
const xp = state?.experience || 0;
|
|
const xpNeeded = getAttunementXPForLevel(level + 1);
|
|
const xpProgress = xpNeeded > 0 ? (xp / xpNeeded) * 100 : 100;
|
|
const isMaxLevel = level >= MAX_ATTUNEMENT_LEVEL;
|
|
|
|
// Get primary mana element info
|
|
const primaryElem = def.primaryManaType ? ELEMENTS[def.primaryManaType] : null;
|
|
|
|
// Get current mana for this attunement's type
|
|
const currentMana = def.primaryManaType ? store.elements[def.primaryManaType]?.current || 0 : 0;
|
|
const maxMana = def.primaryManaType ? store.elements[def.primaryManaType]?.max || 50 : 50;
|
|
|
|
// Calculate level-scaled stats
|
|
const levelMult = Math.pow(1.5, level - 1);
|
|
const scaledRegen = def.rawManaRegen * levelMult;
|
|
const scaledConversion = getAttunementConversionRate(id, level);
|
|
|
|
return (
|
|
<Card
|
|
key={id}
|
|
className={`bg-gray-900/80 transition-all ${
|
|
isActive
|
|
? 'border-2 shadow-lg'
|
|
: isUnlocked
|
|
? 'border-gray-600'
|
|
: 'border-gray-800 opacity-70'
|
|
}`}
|
|
style={{
|
|
borderColor: isActive ? def.color : undefined,
|
|
boxShadow: isActive ? `0 0 20px ${def.color}30` : undefined
|
|
}}
|
|
>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-2xl">{def.icon}</span>
|
|
<div>
|
|
<CardTitle className="text-sm" style={{ color: isActive ? def.color : '#9CA3AF' }}>
|
|
{def.name}
|
|
</CardTitle>
|
|
<div className="text-xs text-gray-500">
|
|
{ATTUNEMENT_SLOT_NAMES[def.slot]}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{!isUnlocked && (
|
|
<Lock className="w-4 h-4 text-gray-600" />
|
|
)}
|
|
{isActive && (
|
|
<Badge className="text-xs" style={{ backgroundColor: `${def.color}30`, color: def.color }}>
|
|
Lv.{level}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<p className="text-xs text-gray-400">{def.desc}</p>
|
|
|
|
{/* Mana Type */}
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-gray-500">Primary Mana</span>
|
|
{primaryElem ? (
|
|
<span style={{ color: primaryElem.color }}>
|
|
{primaryElem.sym} {primaryElem.name}
|
|
</span>
|
|
) : (
|
|
<span className="text-purple-400">From Pacts</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Mana bar (only for attunements with primary type) */}
|
|
{primaryElem && isActive && (
|
|
<div className="space-y-1">
|
|
<Progress
|
|
value={(currentMana / maxMana) * 100}
|
|
className="h-2 bg-gray-800"
|
|
/>
|
|
<div className="flex justify-between text-xs text-gray-500">
|
|
<span>{currentMana.toFixed(1)}</span>
|
|
<span>/{maxMana}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Stats with level scaling */}
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
<div className="p-2 bg-gray-800/50 rounded">
|
|
<div className="text-gray-500">Raw Regen</div>
|
|
<div className="text-green-400 font-semibold">
|
|
+{scaledRegen.toFixed(2)}/hr
|
|
{level > 1 && <span className="text-xs ml-1">({((levelMult - 1) * 100).toFixed(0)}% bonus)</span>}
|
|
</div>
|
|
</div>
|
|
<div className="p-2 bg-gray-800/50 rounded">
|
|
<div className="text-gray-500">Conversion</div>
|
|
<div className="text-cyan-400 font-semibold">
|
|
{scaledConversion > 0 ? `${scaledConversion.toFixed(2)}/hr` : '—'}
|
|
{level > 1 && scaledConversion > 0 && <span className="text-xs ml-1">({((levelMult - 1) * 100).toFixed(0)}% bonus)</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* XP Progress Bar */}
|
|
{isUnlocked && state && !isMaxLevel && (
|
|
<div className="space-y-1">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-gray-500 flex items-center gap-1">
|
|
<TrendingUp className="w-3 h-3" />
|
|
XP Progress
|
|
</span>
|
|
<span className="text-amber-400">{xp} / {xpNeeded}</span>
|
|
</div>
|
|
<Progress
|
|
value={xpProgress}
|
|
className="h-2 bg-gray-800"
|
|
/>
|
|
<div className="text-xs text-gray-500">
|
|
{isMaxLevel ? 'Max Level' : `${xpNeeded - xp} XP to Level ${level + 1}`}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Max Level Indicator */}
|
|
{isMaxLevel && (
|
|
<div className="text-xs text-amber-400 text-center font-semibold">
|
|
✨ MAX LEVEL ✨
|
|
</div>
|
|
)}
|
|
|
|
{/* Capabilities */}
|
|
<div className="space-y-1">
|
|
<div className="text-xs text-gray-500">Capabilities</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{def.capabilities.map(cap => (
|
|
<Badge key={cap} variant="outline" className="text-xs">
|
|
{cap === 'enchanting' && '✨ Enchanting'}
|
|
{cap === 'disenchanting' && '🔄 Disenchant'}
|
|
{cap === 'pacts' && '🤝 Pacts'}
|
|
{cap === 'guardianPowers' && '💜 Guardian Powers'}
|
|
{cap === 'elementalMastery' && '🌟 Elem. Mastery'}
|
|
{cap === 'golemCrafting' && '🗿 Golems'}
|
|
{cap === 'gearCrafting' && '⚒️ Gear'}
|
|
{cap === 'earthShaping' && '⛰️ Earth Shaping'}
|
|
{!['enchanting', 'disenchanting', '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'}
|
|
{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>
|
|
);
|
|
}
|