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:
2026-03-27 18:51:13 +00:00
parent e0a3d82dea
commit a1b15cea74
7 changed files with 531 additions and 29 deletions

View File

@@ -13,8 +13,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { RotateCcw } from 'lucide-react'; import { RotateCcw } from 'lucide-react';
import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab, AttunementsTab } from '@/components/game/tabs'; import { CraftingTab, SpireTab, SpellsTab, LabTab, SkillsTab, StatsTab, EquipmentTab, AttunementsTab, DebugTab } from '@/components/game/tabs';
import { ComboMeter, ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game'; import { ActionButtons, CalendarDisplay, ManaDisplay, TimeDisplay } from '@/components/game';
import { LootInventoryDisplay } from '@/components/game/LootInventory'; import { LootInventoryDisplay } from '@/components/game/LootInventory';
import { AchievementsDisplay } from '@/components/game/AchievementsDisplay'; import { AchievementsDisplay } from '@/components/game/AchievementsDisplay';
@@ -166,8 +166,6 @@ export default function ManaLoopGame() {
isPaused={store.isPaused} isPaused={store.isPaused}
togglePause={store.togglePause} togglePause={store.togglePause}
/> />
<ComboMeter combo={store.combo} isClimbing={store.currentAction === 'climb'} />
</div> </div>
</div> </div>
</header> </header>
@@ -181,9 +179,12 @@ export default function ManaLoopGame() {
rawMana={store.rawMana} rawMana={store.rawMana}
maxMana={maxMana} maxMana={maxMana}
effectiveRegen={effectiveRegen} effectiveRegen={effectiveRegen}
meditationMultiplier={meditationMultiplier}
clickMana={clickMana}
isGathering={isGathering} isGathering={isGathering}
onGatherStart={handleGatherStart} onGatherStart={handleGatherStart}
onGatherEnd={handleGatherEnd} onGatherEnd={handleGatherEnd}
elements={store.elements}
/> />
{/* Action Buttons */} {/* Action Buttons */}
@@ -238,6 +239,7 @@ export default function ManaLoopGame() {
<TabsTrigger value="crafting" className="text-xs px-2 py-1">🔧 Craft</TabsTrigger> <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="lab" className="text-xs px-2 py-1">🔬 Lab</TabsTrigger>
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</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> <TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
</TabsList> </TabsList>
@@ -288,6 +290,10 @@ export default function ManaLoopGame() {
<TabsContent value="grimoire"> <TabsContent value="grimoire">
{renderGrimoireTab()} {renderGrimoireTab()}
</TabsContent> </TabsContent>
<TabsContent value="debug">
<DebugTab store={store} />
</TabsContent>
</Tabs> </Tabs>
</div> </div>
</main> </main>

View File

@@ -3,8 +3,10 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Card, CardContent } from '@/components/ui/card'; 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 { fmt, fmtDec } from '@/lib/game/store';
import { ELEMENTS } from '@/lib/game/constants';
import { useState } from 'react';
interface ManaDisplayProps { interface ManaDisplayProps {
rawMana: number; rawMana: number;
@@ -15,6 +17,7 @@ interface ManaDisplayProps {
isGathering: boolean; isGathering: boolean;
onGatherStart: () => void; onGatherStart: () => void;
onGatherEnd: () => void; onGatherEnd: () => void;
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
} }
export function ManaDisplay({ export function ManaDisplay({
@@ -26,10 +29,19 @@ export function ManaDisplay({
isGathering, isGathering,
onGatherStart, onGatherStart,
onGatherEnd, onGatherEnd,
elements,
}: ManaDisplayProps) { }: 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 ( return (
<Card className="bg-gray-900/80 border-gray-700"> <Card className="bg-gray-900/80 border-gray-700">
<CardContent className="pt-4 space-y-3"> <CardContent className="pt-4 space-y-3">
{/* Raw Mana - Main Display */}
<div> <div>
<div className="flex items-baseline gap-1"> <div className="flex items-baseline gap-1">
<span className="text-3xl font-bold game-mono text-blue-400">{fmt(rawMana)}</span> <span className="text-3xl font-bold game-mono text-blue-400">{fmt(rawMana)}</span>
@@ -57,6 +69,54 @@ export function ManaDisplay({
Gather +{clickMana} Mana Gather +{clickMana} Mana
{isGathering && <span className="ml-2 text-xs">(Holding...)</span>} {isGathering && <span className="ml-2 text-xs">(Holding...)</span>}
</Button> </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> </CardContent>
</Card> </Card>
); );

View File

@@ -1,13 +1,13 @@
'use client'; '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 { ELEMENTS } from '@/lib/game/constants';
import type { GameStore, AttunementState } from '@/lib/game/types'; import type { GameStore, AttunementState } from '@/lib/game/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { Lock, Sparkles } from 'lucide-react'; import { Lock, Sparkles, TrendingUp } from 'lucide-react';
export interface AttunementsTabProps { export interface AttunementsTabProps {
store: GameStore; store: GameStore;
@@ -38,7 +38,7 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
<CardContent> <CardContent>
<p className="text-sm text-gray-400 mb-3"> <p className="text-sm text-gray-400 mb-3">
Attunements are magical bonds tied to specific body locations. Each attunement grants unique capabilities, 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> </p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Badge className="bg-teal-900/50 text-teal-300"> <Badge className="bg-teal-900/50 text-teal-300">
@@ -57,6 +57,11 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
const state = attunements[id]; const state = attunements[id];
const isActive = state?.active; const isActive = state?.active;
const isUnlocked = state?.active || def.unlocked; 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 // Get primary mana element info
const primaryElem = def.primaryManaType ? ELEMENTS[def.primaryManaType] : null; 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 currentMana = def.primaryManaType ? store.elements[def.primaryManaType]?.current || 0 : 0;
const maxMana = def.primaryManaType ? store.elements[def.primaryManaType]?.max || 50 : 50; 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 ( return (
<Card <Card
key={id} key={id}
@@ -98,7 +108,7 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
)} )}
{isActive && ( {isActive && (
<Badge className="text-xs" style={{ backgroundColor: `${def.color}30`, color: def.color }}> <Badge className="text-xs" style={{ backgroundColor: `${def.color}30`, color: def.color }}>
Active Lv.{level}
</Badge> </Badge>
)} )}
</div> </div>
@@ -134,20 +144,51 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
)} )}
</div> </div>
{/* Stats */} {/* Stats with level scaling */}
<div className="grid grid-cols-2 gap-2 text-xs"> <div className="grid grid-cols-2 gap-2 text-xs">
<div className="p-2 bg-gray-800/50 rounded"> <div className="p-2 bg-gray-800/50 rounded">
<div className="text-gray-500">Raw Regen</div> <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>
<div className="p-2 bg-gray-800/50 rounded"> <div className="p-2 bg-gray-800/50 rounded">
<div className="text-gray-500">Conversion</div> <div className="text-gray-500">Conversion</div>
<div className="text-cyan-400 font-semibold"> <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> </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 */} {/* Capabilities */}
<div className="space-y-1"> <div className="space-y-1">
<div className="text-xs text-gray-500">Capabilities</div> <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"> <Badge key={cap} variant="outline" className="text-xs">
{cap === 'enchanting' && '✨ Enchanting'} {cap === 'enchanting' && '✨ Enchanting'}
{cap === 'disenchanting' && '🔄 Disenchant'} {cap === 'disenchanting' && '🔄 Disenchant'}
{cap === 'scrollCrafting' && '📜 Scrolls'}
{cap === 'pacts' && '🤝 Pacts'} {cap === 'pacts' && '🤝 Pacts'}
{cap === 'guardianPowers' && '💜 Guardian Powers'} {cap === 'guardianPowers' && '💜 Guardian Powers'}
{cap === 'elementalMastery' && '🌟 Elem. Mastery'} {cap === 'elementalMastery' && '🌟 Elem. Mastery'}
{cap === 'golemCrafting' && '🗿 Golems'} {cap === 'golemCrafting' && '🗿 Golems'}
{cap === 'gearCrafting' && '⚒️ Gear'} {cap === 'gearCrafting' && '⚒️ Gear'}
{cap === 'earthShaping' && '⛰️ Earth Shaping'} {cap === 'earthShaping' && '⛰️ Earth Shaping'}
{!['enchanting', 'disenchanting', 'scrollCrafting', 'pacts', 'guardianPowers', {!['enchanting', 'disenchanting', 'pacts', 'guardianPowers',
'elementalMastery', 'golemCrafting', 'gearCrafting', 'earthShaping'].includes(cap) && cap} 'elementalMastery', 'golemCrafting', 'gearCrafting', 'earthShaping'].includes(cap) && cap}
</Badge> </Badge>
))} ))}
@@ -176,13 +216,6 @@ export function AttunementsTab({ store }: AttunementsTabProps) {
🔒 {def.unlockCondition} 🔒 {def.unlockCondition}
</div> </div>
)} )}
{/* Level for unlocked attunements */}
{isUnlocked && state && (
<div className="text-xs text-gray-400">
Level {state.level} {state.experience} XP
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
); );

View 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>
);
}

View File

@@ -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 { fmt, fmtDec, getFloorElement, canAffordSpellCost } from '@/lib/game/store';
import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats'; import { getActiveEquipmentSpells, getTotalDPS } from '@/lib/game/computed-stats';
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting'; 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'; import { getUnifiedEffects } from '@/lib/game/effects';
interface SpireTabProps { interface SpireTabProps {
@@ -200,9 +200,6 @@ export function SpireTab({ store }: SpireTabProps) {
</CardContent> </CardContent>
</Card> </Card>
{/* Combo Meter - Shows when climbing or has active combo */}
<ComboMeter combo={store.combo} isClimbing={store.currentAction === 'climb'} />
{/* Current Study (if any) */} {/* Current Study (if any) */}
{store.currentStudyTarget && ( {store.currentStudyTarget && (
<Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2"> <Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2">

View File

@@ -9,3 +9,4 @@ export { SkillsTab } from './SkillsTab';
export { StatsTab } from './StatsTab'; export { StatsTab } from './StatsTab';
export { EquipmentTab } from './EquipmentTab'; export { EquipmentTab } from './EquipmentTab';
export { AttunementsTab } from './AttunementsTab'; export { AttunementsTab } from './AttunementsTab';
export { DebugTab } from './DebugTab';

View File

@@ -31,7 +31,7 @@ export const ATTUNEMENTS_DEF: Record<string, AttunementDef> = {
rawManaRegen: 0.5, rawManaRegen: 0.5,
conversionRate: 0.2, // Converts 0.2 raw mana to transference per hour conversionRate: 0.2, // Converts 0.2 raw mana to transference per hour
unlocked: true, // Starting attunement unlocked: true, // Starting attunement
capabilities: ['enchanting', 'disenchanting', 'scrollCrafting'], capabilities: ['enchanting', 'disenchanting'],
skillCategories: ['enchant', 'effectResearch'], skillCategories: ['enchant', 'effectResearch'],
}, },
@@ -88,13 +88,38 @@ export function getUnlockedAttunements(attunements: Record<string, { active: boo
.filter(Boolean); .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 { export function getTotalAttunementRegen(attunements: Record<string, { active: boolean; level: number; experience: number }>): number {
return Object.entries(attunements) return Object.entries(attunements)
.filter(([id, state]) => state.active) .filter(([, state]) => state.active)
.reduce((total, [id]) => total + (ATTUNEMENTS_DEF[id]?.rawManaRegen || 0), 0); .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 // Helper function to get mana types from active attunements and pacts
export function getAttunementManaTypes( export function getAttunementManaTypes(
attunements: Record<string, { active: boolean; level: number; experience: number }>, attunements: Record<string, { active: boolean; level: number; experience: number }>,