feat: add attunement leveling system, debug tab, and UI improvements
- Add mana pools display to ManaDisplay component with collapsible elements - Add Debug tab with reset game, mana debug, time control, attunement unlock, element unlock - Remove ComboMeter from UI (header and SpireTab) - Remove 'scrollCrafting' capability from Enchanter attunement - Add attunement level scaling (exponential with level) - Add XP progress bar and level display in AttunementsTab - Add getAttunementConversionRate and getAttunementXPForLevel functions - MAX_ATTUNEMENT_LEVEL = 10 with 3^level XP scaling
This commit is contained in:
@@ -13,8 +13,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab, AttunementsTab } from '@/components/game/tabs';
|
||||
import { ComboMeter, ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
|
||||
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab, AttunementsTab, DebugTab } from '@/components/game/tabs';
|
||||
import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
|
||||
import { LootInventoryDisplay } from '@/components/game/LootInventory';
|
||||
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
|
||||
|
||||
@@ -166,8 +166,6 @@ export default function ManaLoopGame() {
|
||||
isPaused={store.isPaused}
|
||||
togglePause={store.togglePause}
|
||||
/>
|
||||
|
||||
<ComboMeter combo={store.combo} isClimbing={store.currentAction === 'climb'} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -181,9 +179,12 @@ export default function ManaLoopGame() {
|
||||
rawMana={store.rawMana}
|
||||
maxMana={maxMana}
|
||||
effectiveRegen={effectiveRegen}
|
||||
meditationMultiplier={meditationMultiplier}
|
||||
clickMana={clickMana}
|
||||
isGathering={isGathering}
|
||||
onGatherStart={handleGatherStart}
|
||||
onGatherEnd={handleGatherEnd}
|
||||
elements={store.elements}
|
||||
/>
|
||||
|
||||
{/* Action Buttons */}
|
||||
@@ -238,6 +239,7 @@ export default function ManaLoopGame() {
|
||||
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger>
|
||||
<TabsTrigger value="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
|
||||
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
|
||||
<TabsTrigger value="debug" className="text-xs px-2 py-1">🔧 Debug</TabsTrigger>
|
||||
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -288,6 +290,10 @@ export default function ManaLoopGame() {
|
||||
<TabsContent value="grimoire">
|
||||
{renderGrimoireTab()}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="debug">
|
||||
<DebugTab store={store} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Zap } from 'lucide-react';
|
||||
import { Zap, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { fmt, fmtDec } from '@/lib/game/store';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ManaDisplayProps {
|
||||
rawMana: number;
|
||||
@@ -15,6 +17,7 @@ interface ManaDisplayProps {
|
||||
isGathering: boolean;
|
||||
onGatherStart: () => void;
|
||||
onGatherEnd: () => void;
|
||||
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||
}
|
||||
|
||||
export function ManaDisplay({
|
||||
@@ -26,10 +29,19 @@ export function ManaDisplay({
|
||||
isGathering,
|
||||
onGatherStart,
|
||||
onGatherEnd,
|
||||
elements,
|
||||
}: ManaDisplayProps) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
// Get unlocked elements sorted by current amount
|
||||
const unlockedElements = Object.entries(elements)
|
||||
.filter(([, state]) => state.unlocked)
|
||||
.sort((a, b) => b[1].current - a[1].current);
|
||||
|
||||
return (
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardContent className="pt-4 space-y-3">
|
||||
{/* Raw Mana - Main Display */}
|
||||
<div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-3xl font-bold game-mono text-blue-400">{fmt(rawMana)}</span>
|
||||
@@ -57,6 +69,54 @@ export function ManaDisplay({
|
||||
Gather +{clickMana} Mana
|
||||
{isGathering && <span className="ml-2 text-xs">(Holding...)</span>}
|
||||
</Button>
|
||||
|
||||
{/* Elemental Mana Pools */}
|
||||
{unlockedElements.length > 0 && (
|
||||
<div className="border-t border-gray-700 pt-3 mt-3">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center justify-between w-full text-xs text-gray-400 hover:text-gray-300 mb-2"
|
||||
>
|
||||
<span>Elemental Mana ({unlockedElements.length})</span>
|
||||
{expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{unlockedElements.map(([id, state]) => {
|
||||
const elem = ELEMENTS[id];
|
||||
if (!elem) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="p-2 rounded bg-gray-800/50 border border-gray-700"
|
||||
>
|
||||
<div className="flex items-center gap-1 mb-1">
|
||||
<span style={{ color: elem.color }}>{elem.sym}</span>
|
||||
<span className="text-xs font-medium" style={{ color: elem.color }}>
|
||||
{elem.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-gray-700 rounded-full overflow-hidden mb-1">
|
||||
<div
|
||||
className="h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${Math.min(100, (state.current / state.max) * 100)}%`,
|
||||
backgroundColor: elem.color
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 game-mono">
|
||||
{fmt(state.current)}/{fmt(state.max)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getTotalAttunementRegen, getAvailableSkillCategories } from '@/lib/game/data/attunements';
|
||||
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 } from 'lucide-react';
|
||||
import { Lock, Sparkles, TrendingUp } from 'lucide-react';
|
||||
|
||||
export interface AttunementsTabProps {
|
||||
store: GameStore;
|
||||
@@ -38,7 +38,7 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
<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.
|
||||
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">
|
||||
@@ -57,6 +57,11 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
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;
|
||||
@@ -65,6 +70,11 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
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}
|
||||
@@ -98,7 +108,7 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
)}
|
||||
{isActive && (
|
||||
<Badge className="text-xs" style={{ backgroundColor: `${def.color}30`, color: def.color }}>
|
||||
Active
|
||||
Lv.{level}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
@@ -134,20 +144,51 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
{/* 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">+{def.rawManaRegen}/hr</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">
|
||||
{def.conversionRate > 0 ? `${def.conversionRate}/hr` : '—'}
|
||||
{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>
|
||||
@@ -156,14 +197,13 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
<Badge key={cap} variant="outline" className="text-xs">
|
||||
{cap === 'enchanting' && '✨ Enchanting'}
|
||||
{cap === 'disenchanting' && '🔄 Disenchant'}
|
||||
{cap === 'scrollCrafting' && '📜 Scrolls'}
|
||||
{cap === 'pacts' && '🤝 Pacts'}
|
||||
{cap === 'guardianPowers' && '💜 Guardian Powers'}
|
||||
{cap === 'elementalMastery' && '🌟 Elem. Mastery'}
|
||||
{cap === 'golemCrafting' && '🗿 Golems'}
|
||||
{cap === 'gearCrafting' && '⚒️ Gear'}
|
||||
{cap === 'earthShaping' && '⛰️ Earth Shaping'}
|
||||
{!['enchanting', 'disenchanting', 'scrollCrafting', 'pacts', 'guardianPowers',
|
||||
{!['enchanting', 'disenchanting', 'pacts', 'guardianPowers',
|
||||
'elementalMastery', 'golemCrafting', 'gearCrafting', 'earthShaping'].includes(cap) && cap}
|
||||
</Badge>
|
||||
))}
|
||||
@@ -176,13 +216,6 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
|
||||
🔒 {def.unlockCondition}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Level for unlocked attunements */}
|
||||
{isUnlocked && state && (
|
||||
<div className="text-xs text-gray-400">
|
||||
Level {state.level} • {state.experience} XP
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
380
src/components/game/tabs/DebugTab.tsx
Normal file
380
src/components/game/tabs/DebugTab.tsx
Normal file
@@ -0,0 +1,380 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
RotateCcw, Bug, Plus, Minus, Lock, Unlock, Zap,
|
||||
Clock, Star, AlertTriangle, Sparkles, Settings
|
||||
} from 'lucide-react';
|
||||
import type { GameStore } from '@/lib/game/types';
|
||||
import { ATTUNEMENTS_DEF } from '@/lib/game/data/attunements';
|
||||
import { ELEMENTS } from '@/lib/game/constants';
|
||||
import { fmt } from '@/lib/game/store';
|
||||
|
||||
interface DebugTabProps {
|
||||
store: GameStore;
|
||||
}
|
||||
|
||||
export function DebugTab({ store }: DebugTabProps) {
|
||||
const [confirmReset, setConfirmReset] = useState(false);
|
||||
|
||||
const handleReset = () => {
|
||||
if (confirmReset) {
|
||||
store.resetGame();
|
||||
setConfirmReset(false);
|
||||
} else {
|
||||
setConfirmReset(true);
|
||||
setTimeout(() => setConfirmReset(false), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddMana = (amount: number) => {
|
||||
// Use gatherMana multiple times to add mana
|
||||
for (let i = 0; i < amount; i++) {
|
||||
store.gatherMana();
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlockAttunement = (id: string) => {
|
||||
// Debug action to unlock attunements
|
||||
if (store.debugUnlockAttunement) {
|
||||
store.debugUnlockAttunement(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnlockElement = (element: string) => {
|
||||
store.unlockElement(element);
|
||||
};
|
||||
|
||||
const handleAddElementalMana = (element: string, amount: number) => {
|
||||
const elem = store.elements[element];
|
||||
if (elem?.unlocked) {
|
||||
// Add directly to element pool - need to implement in store
|
||||
if (store.debugAddElementalMana) {
|
||||
store.debugAddElementalMana(element, amount);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetTime = (day: number, hour: number) => {
|
||||
if (store.debugSetTime) {
|
||||
store.debugSetTime(day, hour);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddAttunementXP = (id: string, amount: number) => {
|
||||
if (store.debugAddAttunementXP) {
|
||||
store.debugAddAttunementXP(id, amount);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Warning Banner */}
|
||||
<Card className="bg-amber-900/20 border-amber-600/50">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-center gap-2 text-amber-400">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
<span className="font-semibold">Debug Mode</span>
|
||||
</div>
|
||||
<p className="text-sm text-amber-300/70 mt-1">
|
||||
These tools are for development and testing. Using them may break game balance or save data.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Game Reset */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-red-400 text-sm flex items-center gap-2">
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
Game Reset
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-xs text-gray-400">
|
||||
Reset all game progress and start fresh. This cannot be undone.
|
||||
</p>
|
||||
<Button
|
||||
className={`w-full ${confirmReset ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'}`}
|
||||
onClick={handleReset}
|
||||
>
|
||||
{confirmReset ? (
|
||||
<>
|
||||
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||
Click Again to Confirm Reset
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RotateCcw className="w-4 h-4 mr-2" />
|
||||
Reset Game
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Mana Debug */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-blue-400 text-sm flex items-center gap-2">
|
||||
<Zap className="w-4 h-4" />
|
||||
Mana Debug
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-xs text-gray-400 mb-2">
|
||||
Current: {fmt(store.rawMana)} / {fmt(store.getMaxMana())}
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(10)}>
|
||||
<Plus className="w-3 h-3 mr-1" /> +10
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(100)}>
|
||||
<Plus className="w-3 h-3 mr-1" /> +100
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(1000)}>
|
||||
<Plus className="w-3 h-3 mr-1" /> +1K
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddMana(10000)}>
|
||||
<Plus className="w-3 h-3 mr-1" /> +10K
|
||||
</Button>
|
||||
</div>
|
||||
<Separator className="bg-gray-700" />
|
||||
<div className="text-xs text-gray-400">Fill to max:</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
onClick={() => {
|
||||
const max = store.getMaxMana();
|
||||
const current = store.rawMana;
|
||||
for (let i = 0; i < Math.floor(max - current); i++) {
|
||||
store.gatherMana();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Fill Mana
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Time Control */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-amber-400 text-sm flex items-center gap-2">
|
||||
<Clock className="w-4 h-4" />
|
||||
Time Control
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="text-xs text-gray-400">
|
||||
Current: Day {store.day}, Hour {store.hour}
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button size="sm" variant="outline" onClick={() => handleSetTime(1, 0)}>
|
||||
Day 1
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSetTime(10, 0)}>
|
||||
Day 10
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSetTime(20, 0)}>
|
||||
Day 20
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleSetTime(30, 0)}>
|
||||
Day 30
|
||||
</Button>
|
||||
</div>
|
||||
<Separator className="bg-gray-700" />
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => store.togglePause()}
|
||||
>
|
||||
{store.paused ? '▶ Resume' : '⏸ Pause'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Attunement Unlock */}
|
||||
<Card className="bg-gray-900/80 border-gray-700">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-purple-400 text-sm flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
Attunements
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Object.entries(ATTUNEMENTS_DEF).map(([id, def]) => {
|
||||
const isActive = store.attunements?.[id]?.active;
|
||||
const level = store.attunements?.[id]?.level || 1;
|
||||
const xp = store.attunements?.[id]?.experience || 0;
|
||||
|
||||
return (
|
||||
<div key={id} className="flex items-center justify-between p-2 bg-gray-800/50 rounded">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{def.icon}</span>
|
||||
<div>
|
||||
<div className="text-sm font-medium">{def.name}</div>
|
||||
{isActive && (
|
||||
<div className="text-xs text-gray-400">Lv.{level} • {xp} XP</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{!isActive && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleUnlockAttunement(id)}
|
||||
>
|
||||
<Unlock className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
{isActive && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleAddAttunementXP(id, 50)}
|
||||
>
|
||||
+50 XP
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleAddAttunementXP(id, 500)}
|
||||
>
|
||||
+500 XP
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Element Unlock */}
|
||||
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-green-400 text-sm flex items-center gap-2">
|
||||
<Star className="w-4 h-4" />
|
||||
Elemental Mana
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-2">
|
||||
{Object.entries(ELEMENTS).map(([id, def]) => {
|
||||
const elem = store.elements[id];
|
||||
const isUnlocked = elem?.unlocked;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`p-2 rounded border ${
|
||||
isUnlocked ? 'border-gray-600' : 'border-gray-800 opacity-60'
|
||||
}`}
|
||||
style={{
|
||||
borderColor: isUnlocked ? def.color : undefined
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span style={{ color: def.color }}>{def.sym}</span>
|
||||
{!isUnlocked && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-5 w-5 p-0"
|
||||
onClick={() => handleUnlockElement(id)}
|
||||
>
|
||||
<Lock className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: def.color }}>{def.name}</div>
|
||||
{isUnlocked && (
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{elem.current.toFixed(0)}/{elem.max}
|
||||
</div>
|
||||
)}
|
||||
{isUnlocked && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="h-5 w-full mt-1 text-xs"
|
||||
onClick={() => handleAddElementalMana(id, 100)}
|
||||
>
|
||||
+100
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Skills Debug */}
|
||||
<Card className="bg-gray-900/80 border-gray-700 md:col-span-2">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-cyan-400 text-sm flex items-center gap-2">
|
||||
<Settings className="w-4 h-4" />
|
||||
Quick Actions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Unlock all base elements
|
||||
['fire', 'water', 'air', 'earth', 'light', 'dark', 'life', 'death'].forEach(e => {
|
||||
if (!store.elements[e]?.unlocked) {
|
||||
store.unlockElement(e);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Unlock All Base Elements
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Unlock utility elements
|
||||
['mental', 'transference', 'force'].forEach(e => {
|
||||
if (!store.elements[e]?.unlocked) {
|
||||
store.unlockElement(e);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Unlock Utility Elements
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Max floor
|
||||
if (store.debugSetFloor) {
|
||||
store.debugSetFloor(100);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Skip to Floor 100
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF } from '@/lib/game/constant
|
||||
import { fmt, fmtDec, getFloorElement, canAffordSpellCost } from '@/lib/game/store';
|
||||
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
|
||||
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
|
||||
import { ComboMeter, CraftingProgress, StudyProgress } from '@/components/game';
|
||||
import { CraftingProgress, StudyProgress } from '@/components/game';
|
||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||
|
||||
interface SpireTabProps {
|
||||
@@ -200,9 +200,6 @@ export function SpireTab({ store }: SpireTabProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Combo Meter - Shows when climbing or has active combo */}
|
||||
<ComboMeter combo={store.combo} isClimbing={store.currentAction === 'climb'} />
|
||||
|
||||
{/* Current Study (if any) */}
|
||||
{store.currentStudyTarget && (
|
||||
<Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2">
|
||||
|
||||
@@ -9,3 +9,4 @@ export { SkillsTab } from './SkillsTab';
|
||||
export { StatsTab } from './StatsTab';
|
||||
export { EquipmentTab } from './EquipmentTab';
|
||||
export { AttunementsTab } from './AttunementsTab';
|
||||
export { DebugTab } from './DebugTab';
|
||||
|
||||
@@ -31,7 +31,7 @@ export const ATTUNEMENTS_DEF: Record<string, AttunementDef> = {
|
||||
rawManaRegen: 0.5,
|
||||
conversionRate: 0.2, // Converts 0.2 raw mana to transference per hour
|
||||
unlocked: true, // Starting attunement
|
||||
capabilities: ['enchanting', 'disenchanting', 'scrollCrafting'],
|
||||
capabilities: ['enchanting', 'disenchanting'],
|
||||
skillCategories: ['enchant', 'effectResearch'],
|
||||
},
|
||||
|
||||
@@ -88,13 +88,38 @@ export function getUnlockedAttunements(attunements: Record<string, { active: boo
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
// Helper function to calculate total raw mana regen from attunements
|
||||
// Helper function to calculate total raw mana regen from attunements (with level scaling)
|
||||
export function getTotalAttunementRegen(attunements: Record<string, { active: boolean; level: number; experience: number }>): number {
|
||||
return Object.entries(attunements)
|
||||
.filter(([id, state]) => state.active)
|
||||
.reduce((total, [id]) => total + (ATTUNEMENTS_DEF[id]?.rawManaRegen || 0), 0);
|
||||
.filter(([, state]) => state.active)
|
||||
.reduce((total, [id, state]) => {
|
||||
const def = ATTUNEMENTS_DEF[id];
|
||||
if (!def) return total;
|
||||
// Exponential scaling: base * (1.5 ^ (level - 1))
|
||||
const levelMult = Math.pow(1.5, (state.level || 1) - 1);
|
||||
return total + def.rawManaRegen * levelMult;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Get conversion rate with level scaling
|
||||
export function getAttunementConversionRate(attunementId: string, level: number): number {
|
||||
const def = ATTUNEMENTS_DEF[attunementId];
|
||||
if (!def || def.conversionRate <= 0) return 0;
|
||||
// Exponential scaling: base * (1.5 ^ (level - 1))
|
||||
return def.conversionRate * Math.pow(1.5, (level || 1) - 1);
|
||||
}
|
||||
|
||||
// XP required for attunement level
|
||||
export function getAttunementXPForLevel(level: number): number {
|
||||
// Level 2: 100 XP, Level 3: 300 XP, Level 4: 900 XP, etc.
|
||||
// Exponential: 100 * (3 ^ (level - 2))
|
||||
if (level <= 1) return 0;
|
||||
return Math.floor(100 * Math.pow(3, level - 2));
|
||||
}
|
||||
|
||||
// Max attunement level
|
||||
export const MAX_ATTUNEMENT_LEVEL = 10;
|
||||
|
||||
// Helper function to get mana types from active attunements and pacts
|
||||
export function getAttunementManaTypes(
|
||||
attunements: Record<string, { active: boolean; level: number; experience: number }>,
|
||||
|
||||
Reference in New Issue
Block a user