refactor: extract components from SpireTab.tsx to reduce below 400 lines
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m11s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m11s
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
'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[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ActivityLog({ activityLog }: 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, 50).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';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
'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';
|
||||||
|
|
||||||
|
export function CombatStatsPanel({
|
||||||
|
activeEquipmentSpells,
|
||||||
|
store,
|
||||||
|
totalDPS,
|
||||||
|
calcDamage,
|
||||||
|
formatSpellCost,
|
||||||
|
getSpellCostColor,
|
||||||
|
SPELLS_DEF,
|
||||||
|
upgradeEffects,
|
||||||
|
canCastSpell,
|
||||||
|
studySpeedMult,
|
||||||
|
storeCurrentAction,
|
||||||
|
}: CombatStatsPanelProps) {
|
||||||
|
const activeGolems = store.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 = store.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(store, spellId))} dmg • {' '}
|
||||||
|
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
||||||
|
{formatSpellCost(spellDef.cost)}
|
||||||
|
</span>
|
||||||
|
{' '}• ⚡ {fmt(Math.floor(calcDamage(store, 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, store.skills);
|
||||||
|
const attackSpeed = getGolemAttackSpeed(summoned.golemId, store.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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { ChevronUp, ChevronDown, Mountain, Shield, Skull, Heart, Wind, ShieldCheck } from 'lucide-react';
|
||||||
|
import { ELEMENTS } from '@/lib/game/constants';
|
||||||
|
import type { FloorControlsProps } from '@/lib/game/types';
|
||||||
|
|
||||||
|
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' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FloorControls({
|
||||||
|
store,
|
||||||
|
climbDirection,
|
||||||
|
isGuardianFloor,
|
||||||
|
currentRoom,
|
||||||
|
currentGuardian,
|
||||||
|
isFloorCleared,
|
||||||
|
floorElemDef,
|
||||||
|
roomType,
|
||||||
|
roomConfig,
|
||||||
|
activeEquipmentSpells,
|
||||||
|
upgradeEffects,
|
||||||
|
floorElem,
|
||||||
|
totalDPS,
|
||||||
|
getEnemyName,
|
||||||
|
calcDamage,
|
||||||
|
SPELLS_DEF,
|
||||||
|
canCastSpell,
|
||||||
|
storeCurrentAction,
|
||||||
|
handleClimb,
|
||||||
|
formatSpellCost,
|
||||||
|
getSpellCostColor,
|
||||||
|
}: 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 || store.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' || store.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, (store.floorHP / store.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(store.floorHP)} / {fmt(store.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 = store.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(store, spellId))} 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
'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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
'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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
'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,34 +1,26 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Progress } from '@/components/ui/progress';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Mountain } from 'lucide-react';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { TooltipProvider, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { BookOpen, ChevronUp, ChevronDown, RotateCcw, X, Mountain, Skull, Zap, Wind, Shield, Heart, ShieldCheck } from 'lucide-react';
|
|
||||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
import type { ActivityLogEntry } from '@/lib/game/types';
|
||||||
import type { GameStore } from '@/lib/game/store';
|
import type { GameStore } from '@/lib/game/store';
|
||||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, ROOM_TYPE_LABELS } from '@/lib/game/constants';
|
import { ELEMENTS, GUARDIANS, SPELLS_DEF } from '@/lib/game/constants';
|
||||||
import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems';
|
import { getEnemyName, calcDamage } from '@/lib/game/store';
|
||||||
import { fmt, fmtDec, getFloorElement, canAffordSpellCost, getEnemyName, calcDamage } 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 { CraftingProgress, StudyProgress } from '@/components/game';
|
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||||
|
import { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting';
|
||||||
|
import { canAffordSpellCost, getFloorElement } from '@/lib/game/store';
|
||||||
|
|
||||||
interface SpireTabProps {
|
// Extracted components
|
||||||
store: GameStore;
|
import { SpireHeader } from './SpireHeader';
|
||||||
simpleMode?: boolean; // When true, only show essential Spire info (for Spire Mode)
|
import { GuardianPanel } from './GuardianPanel';
|
||||||
}
|
import { RoomDisplay } from './RoomDisplay';
|
||||||
|
import { FloorControls } from './FloorControls';
|
||||||
|
import { CombatStatsPanel } from './CombatStatsPanel';
|
||||||
|
import { ActivityLog } from './ActivityLog';
|
||||||
|
|
||||||
// Helper to check if player can enter spire mode
|
// Room type configurations
|
||||||
const canEnterSpireMode = (store: GameStore): boolean => {
|
|
||||||
return !store.spireMode; // Can enter if not already in Spire Mode
|
|
||||||
};
|
|
||||||
|
|
||||||
// Room type configurations for display
|
|
||||||
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
|
const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: string }> = {
|
||||||
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
|
combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' },
|
||||||
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
|
swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' },
|
||||||
@@ -37,7 +29,18 @@ const ROOM_TYPE_CONFIG: Record<string, { label: string; icon: string; color: str
|
|||||||
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
|
puzzle: { label: 'Puzzle', icon: '🧩', color: '#8B5CF6' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface SpireTabProps {
|
||||||
|
store: GameStore;
|
||||||
|
simpleMode?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if player can enter spire mode
|
||||||
|
const canEnterSpireMode = (store: GameStore): boolean => {
|
||||||
|
return !store.spireMode;
|
||||||
|
};
|
||||||
|
|
||||||
export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
||||||
|
// Derived data
|
||||||
const floorElem = getFloorElement(store.currentFloor);
|
const floorElem = getFloorElement(store.currentFloor);
|
||||||
const floorElemDef = ELEMENTS[floorElem];
|
const floorElemDef = ELEMENTS[floorElem];
|
||||||
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
|
const isGuardianFloor = !!GUARDIANS[store.currentFloor];
|
||||||
@@ -45,576 +48,320 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
|||||||
const climbDirection = store.climbDirection || 'up';
|
const climbDirection = store.climbDirection || 'up';
|
||||||
const clearedFloors = store.clearedFloors || {};
|
const clearedFloors = store.clearedFloors || {};
|
||||||
const currentRoom = store.currentRoom;
|
const currentRoom = store.currentRoom;
|
||||||
|
|
||||||
// Check if current floor is cleared (for respawn indicator)
|
|
||||||
const isFloorCleared = clearedFloors[store.currentFloor];
|
const isFloorCleared = clearedFloors[store.currentFloor];
|
||||||
|
|
||||||
// Get room type info
|
|
||||||
const roomType = currentRoom?.roomType || 'combat';
|
const roomType = currentRoom?.roomType || 'combat';
|
||||||
const roomConfig = ROOM_TYPE_CONFIG[roomType] || ROOM_TYPE_CONFIG.combat;
|
const roomConfig = ROOM_TYPE_CONFIG[roomType] || ROOM_TYPE_CONFIG.combat;
|
||||||
|
|
||||||
// Get active equipment spells
|
|
||||||
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
|
const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances);
|
||||||
|
|
||||||
// Get upgrade effects and DPS
|
|
||||||
const upgradeEffects = getUnifiedEffects(store);
|
const upgradeEffects = getUnifiedEffects(store);
|
||||||
const totalDPS = getTotalDPS(store, upgradeEffects, floorElem);
|
const totalDPS = getTotalDPS(store, upgradeEffects, floorElem);
|
||||||
const studySpeedMult = 1; // Base study speed
|
const studySpeedMult = 1;
|
||||||
|
|
||||||
|
// Enemy display info
|
||||||
|
const primaryEnemy = currentRoom?.enemies?.[0] || null;
|
||||||
|
const swarmEnemies = roomType === 'swarm' && currentRoom?.enemies ? currentRoom.enemies : [];
|
||||||
|
|
||||||
|
// Spell casting check
|
||||||
const canCastSpell = (spellId: string): boolean => {
|
const canCastSpell = (spellId: string): boolean => {
|
||||||
const spell = SPELLS_DEF[spellId];
|
const spell = SPELLS_DEF[spellId];
|
||||||
if (!spell) return false;
|
if (!spell) return false;
|
||||||
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
|
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get enemy display info
|
// Climb handler
|
||||||
const getEnemyDisplayInfo = () => {
|
const handleClimb = (direction: 'up' | 'down') => {
|
||||||
if (!currentRoom || !currentRoom.enemies || currentRoom.enemies.length === 0) {
|
if (direction === 'up') {
|
||||||
return { primaryEnemy: null, swarmEnemies: [] };
|
store.startClimbUp();
|
||||||
|
} else {
|
||||||
|
store.startClimbDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
const enemies = currentRoom.enemies;
|
|
||||||
const primaryEnemy = enemies[0];
|
|
||||||
|
|
||||||
// For swarm rooms, return all enemies
|
|
||||||
if (roomType === 'swarm') {
|
|
||||||
return { primaryEnemy: null, swarmEnemies: enemies };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { primaryEnemy, swarmEnemies: [] };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { primaryEnemy, swarmEnemies } = getEnemyDisplayInfo();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<div className="grid gap-4">
|
||||||
<div className={`grid gap-4 ${simpleMode ? 'grid-cols-1' : 'grid-cols-1 lg:grid-cols-2'}`}>
|
{/* Enter Spire Mode - Normal mode only */}
|
||||||
{/* Spire Stats View - Only show in normal mode (not simpleMode) */}
|
{!simpleMode && (
|
||||||
{!simpleMode && (
|
<Card className="bg-gray-900/80 border-amber-600/50">
|
||||||
<>
|
<CardContent className="pt-4">
|
||||||
{/* Enter Spire Mode Button */}
|
<Button
|
||||||
<Card className="bg-gray-900/80 border-amber-600/50">
|
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700"
|
||||||
<CardContent className="pt-4">
|
size="lg"
|
||||||
<Button
|
onClick={() => store.enterSpireMode()}
|
||||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700"
|
disabled={!canEnterSpireMode(store)}
|
||||||
size="lg"
|
>
|
||||||
onClick={() => store.enterSpireMode()}
|
<Mountain className="w-5 h-5 mr-2" />
|
||||||
disabled={!canEnterSpireMode(store)}
|
Enter Spire Mode
|
||||||
>
|
</Button>
|
||||||
<Mountain className="w-5 h-5 mr-2" />
|
<div className="text-xs text-gray-400 text-center mt-2">
|
||||||
Enter Spire Mode
|
Climb the Spire to face guardians and earn pacts
|
||||||
</Button>
|
</div>
|
||||||
<div className="text-xs text-gray-400 text-center mt-2">
|
</CardContent>
|
||||||
Climb the Spire to face guardians and earn pacts
|
</Card>
|
||||||
</div>
|
)}
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Spire Stats Card - Replaces Current Floor stat */}
|
{/* Spire Header */}
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
<SpireHeader
|
||||||
<CardHeader className="pb-2">
|
currentFloor={store.currentFloor}
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Spire Stats</CardTitle>
|
maxFloorReached={store.maxFloorReached}
|
||||||
</CardHeader>
|
signedPacts={store.signedPacts.length}
|
||||||
<CardContent className="space-y-3">
|
isGuardianFloor={isGuardianFloor}
|
||||||
<div className="grid grid-cols-2 gap-3">
|
roomType={roomType}
|
||||||
<div className="p-3 bg-gray-800/50 rounded">
|
roomLabel={roomConfig.label}
|
||||||
<div className="text-2xl font-bold text-amber-400 game-mono">{store.maxFloorReached}</div>
|
roomIcon={roomConfig.icon}
|
||||||
<div className="text-xs text-gray-400">Best Floor</div>
|
roomColor={roomConfig.color}
|
||||||
</div>
|
floorElem={floorElem}
|
||||||
<div className="p-3 bg-gray-800/50 rounded">
|
floorElemDef={floorElemDef}
|
||||||
<div className="text-2xl font-bold text-purple-400 game-mono">{store.signedPacts.length}</div>
|
simpleMode={simpleMode}
|
||||||
<div className="text-xs text-gray-400">Pacts Signed</div>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Current Floor: <strong className="text-gray-200">{store.currentFloor}</strong>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current Floor Card - Only show in Spire Mode (simpleMode) */}
|
{/* Active Spells Card - Spire Mode only */}
|
||||||
{simpleMode && (
|
{simpleMode && (
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
<Card className="bg-gray-900/80 border-gray-700">
|
||||||
<CardHeader className="pb-2">
|
<CardContent className="pt-4 pb-4">
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center justify-between">
|
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider">
|
||||||
<span>Current Floor</span>
|
Active Spells ({activeEquipmentSpells.length})
|
||||||
<Badge
|
</div>
|
||||||
className="ml-2"
|
{activeEquipmentSpells.length > 0 ? (
|
||||||
style={{
|
<div className="space-y-2">
|
||||||
backgroundColor: `${roomConfig.color}20`,
|
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
||||||
color: roomConfig.color,
|
const spellDef = SPELLS_DEF[spellId];
|
||||||
borderColor: `${roomConfig.color}60`
|
if (!spellDef) return null;
|
||||||
}}
|
const spellState = store.equipmentSpellStates?.find(
|
||||||
>
|
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
||||||
{roomConfig.icon} {roomConfig.label}
|
);
|
||||||
</Badge>
|
const progress = spellState?.castProgress || 0;
|
||||||
</CardTitle>
|
const canCast = canCastSpell(spellId);
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
<div className="flex items-baseline gap-2">
|
|
||||||
<span className="text-4xl font-bold game-title" style={{ color: floorElemDef?.color }}>
|
|
||||||
{store.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>
|
|
||||||
|
|
||||||
{/* Guardian Name */}
|
return (
|
||||||
{isGuardianFloor && currentGuardian && (
|
<div key={`${spellId}-${equipmentId}`} className="p-2 rounded bg-gray-800/30 border border-gray-700">
|
||||||
<div className="text-sm font-semibold game-panel-title" style={{ color: floorElemDef?.color }}>
|
|
||||||
⚔️ {currentGuardian.name}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Single Enemy Display (Combat/Speed/Guardian) */}
|
|
||||||
{!isGuardianFloor && primaryEnemy && roomType !== 'swarm' && (
|
|
||||||
<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 */}
|
|
||||||
<div className="flex flex-wrap gap-2 text-xs">
|
|
||||||
{primaryEnemy.armor > 0 && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<Shield className="w-3 h-3 mr-1" />
|
|
||||||
{(primaryEnemy.armor * 100).toFixed(0)}% Armor
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Reduces incoming damage by {(primaryEnemy.armor * 100).toFixed(0)}%</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{primaryEnemy.dodgeChance > 0 && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<Wind className="w-3 h-3 mr-1" />
|
|
||||||
{(primaryEnemy.dodgeChance * 100).toFixed(0)}% Dodge
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Chance to dodge attacks and reduce progress</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{primaryEnemy.healthRegen > 0 && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<Heart className="w-3 h-3 mr-1 text-green-400" />
|
|
||||||
{(primaryEnemy.healthRegen * 100).toFixed(1)}% Regen
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Regenerates {(primaryEnemy.healthRegen * 100).toFixed(1)}% of max HP per tick</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{primaryEnemy.barrier > 0 && (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<ShieldCheck className="w-3 h-3 mr-1 text-blue-400" />
|
|
||||||
{(primaryEnemy.barrier * 100).toFixed(0)}% Barrier
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Has a barrier absorbing {(primaryEnemy.barrier * 100).toFixed(0)}% of max HP before HP takes damage</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Swarm Enemies Display */}
|
|
||||||
{roomType === 'swarm' && swarmEnemies.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="text-xs text-gray-400 font-semibold">
|
|
||||||
Swarm Enemies ({swarmEnemies.length})
|
|
||||||
</div>
|
|
||||||
{swarmEnemies.map((enemy, index) => (
|
|
||||||
<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 justify-between mb-1">
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-sm font-semibold" style={{ color: spellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[spellDef.elem]?.color }}>
|
||||||
<Skull className="w-3 h-3 text-red-400" />
|
{spellDef.name}
|
||||||
<span className="text-xs font-semibold text-gray-300">
|
{spellDef.tier === 0 && <span className="ml-2 bg-gray-600 text-gray-200 text-xs px-1 rounded">Basic</span>}
|
||||||
{enemy.name || `Enemy ${index + 1}`}
|
</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">
|
||||||
|
⚔️ {fmt(calcDamage(store, spellId))} dmg • <span style={{ color: getSpellCostColor(spellDef.cost) }}>{formatSpellCost(spellDef.cost)}</span> {' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||||||
|
</div>
|
||||||
|
{store.currentAction === '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: spellDef.elem === 'raw' ? 'linear-gradient(90deg, #60A5FA99, #60A5FA)' : `linear-gradient(90deg, ${ELEMENTS[spellDef.elem]?.color}99, ${ELEMENTS[spellDef.elem]?.color})` }} />
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
|
||||||
{/* Show enemy properties for swarm enemies */}
|
|
||||||
<div className="flex flex-wrap gap-1 mt-1">
|
|
||||||
{enemy.armor > 0 && (
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<Shield className="w-2 h-2 mr-1" />
|
|
||||||
{(enemy.armor * 100).toFixed(0)}% Armor
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{enemy.healthRegen > 0 && (
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<Heart className="w-2 h-2 mr-1 text-green-400" />
|
|
||||||
{(enemy.healthRegen * 100).toFixed(1)}% Regen
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{enemy.barrier > 0 && (
|
|
||||||
<Badge variant="outline" className="text-xs py-0">
|
|
||||||
<ShieldCheck className="w-2 h-2 mr-1 text-blue-400" />
|
|
||||||
{(enemy.barrier * 100).toFixed(0)}% Barrier
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
</div>
|
})}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Puzzle Room Display */}
|
|
||||||
{roomType === 'puzzle' && (
|
|
||||||
<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">
|
|
||||||
{currentRoom.puzzleId ? currentRoom.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>{((currentRoom.puzzleProgress || 0) * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress
|
|
||||||
value={Math.min(100, (currentRoom.puzzleProgress || 0) * 100)}
|
|
||||||
className="h-2 bg-gray-800"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Floor HP Bar (for non-swarm, non-puzzle) */}
|
|
||||||
{roomType !== 'swarm' && roomType !== 'puzzle' && (
|
|
||||||
<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, (store.floorHP / store.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(store.floorHP)} / {fmt(store.floorMaxHP)} HP</span>
|
|
||||||
<span>DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="text-sm text-gray-400">
|
|
||||||
Best: Floor <strong className="text-gray-200">{store.maxFloorReached}</strong> •
|
|
||||||
Pacts: <strong className="text-amber-400">{store.signedPacts.length}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
) : (
|
||||||
</Card>
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Active Spells Card - Only show in Spire Mode (simpleMode) */}
|
{/* Summoned Golems */}
|
||||||
{simpleMode && (
|
{simpleMode && store.golemancy.summonedGolems.length > 0 && (
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
<Card className="bg-gray-900/80 border-amber-600/50">
|
||||||
<CardHeader className="pb-2">
|
<CardContent className="pt-4 pb-4">
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">
|
<div className="text-amber-400 text-xs font-semibold mb-3 uppercase tracking-wider flex items-center gap-2">
|
||||||
Active Spells ({activeEquipmentSpells.length})
|
<Mountain className="w-4 h-4" />
|
||||||
</CardTitle>
|
Active Golems ({store.golemancy.summonedGolems.length})
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent className="space-y-3">
|
<div className="space-y-2">
|
||||||
{activeEquipmentSpells.length > 0 ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{activeEquipmentSpells.map(({ spellId, equipmentId }) => {
|
|
||||||
const spellDef = SPELLS_DEF[spellId];
|
|
||||||
if (!spellDef) return null;
|
|
||||||
|
|
||||||
const spellState = store.equipmentSpellStates?.find(
|
|
||||||
s => s.spellId === spellId && s.sourceEquipment === equipmentId
|
|
||||||
);
|
|
||||||
const progress = spellState?.castProgress || 0;
|
|
||||||
const canCast = canAffordSpellCost(spellDef.cost, store.rawMana, store.elements);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={`${spellId}-${equipmentId}`} className="p-2 rounded border border-gray-700 bg-gray-800/30">
|
|
||||||
<div className="flex items-center justify-between mb-1">
|
|
||||||
<div className="text-sm font-semibold game-panel-title" 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>}
|
|
||||||
</div>
|
|
||||||
<span className={`text-xs ${canCast ? 'text-green-400' : 'text-red-400'}`}>
|
|
||||||
{canCast ? '✓' : '✗'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 game-mono mb-1">
|
|
||||||
⚔️ {fmt(calcDamage(store, spellId))} dmg/cast •
|
|
||||||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
|
||||||
{' '}{formatSpellCost(spellDef.cost)}
|
|
||||||
</span>
|
|
||||||
{' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cast progress bar when climbing */}
|
|
||||||
{store.currentAction === '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>
|
|
||||||
<Progress value={Math.min(100, progress * 100)} className="h-1.5 bg-gray-700" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{spellDef.effects && spellDef.effects.length > 0 && (
|
|
||||||
<div className="flex gap-1 flex-wrap mt-1">
|
|
||||||
{spellDef.effects.map((eff, i) => (
|
|
||||||
<Badge key={i} variant="outline" className="text-xs py-0">
|
|
||||||
{eff.type === 'burn' && `🔥 Burn`}
|
|
||||||
{eff.type === 'freeze' && `❄️ Freeze`}
|
|
||||||
{eff.type === 'stun' && `⚡ Stun`}
|
|
||||||
{eff.type === 'armor_pierce' && `🗡️ Pierce`}
|
|
||||||
{eff.type === 'buff' && `⬆ Buff`}
|
|
||||||
{eff.type === 'chain' && `⛓️ Chain`}
|
|
||||||
{eff.type === 'aoe' && `💥 AOE`}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Summoned Golems Card - Show in Spire Mode (simpleMode) or if have golems */}
|
|
||||||
{simpleMode && store.golemancy.summonedGolems.length > 0 && (
|
|
||||||
<Card className="bg-gray-900/80 border-amber-600/50">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center gap-2">
|
|
||||||
<Mountain className="w-4 h-4" />
|
|
||||||
Active Golems ({store.golemancy.summonedGolems.length})
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-2">
|
|
||||||
{store.golemancy.summonedGolems.map((summoned) => {
|
{store.golemancy.summonedGolems.map((summoned) => {
|
||||||
const golemDef = GOLEMS_DEF[summoned.golemId];
|
const golemDef = getGolemDef(summoned.golemId);
|
||||||
if (!golemDef) return null;
|
if (!golemDef) return null;
|
||||||
|
|
||||||
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
const elemColor = ELEMENTS[golemDef.baseManaType]?.color || '#888';
|
||||||
const damage = getGolemDamage(summoned.golemId, store.skills);
|
const damage = getGolemDamage(summoned.golemId, store.skills);
|
||||||
const attackSpeed = getGolemAttackSpeed(summoned.golemId, store.skills);
|
const attackSpeed = getGolemAttackSpeed(summoned.golemId, store.skills);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={summoned.golemId} className="p-2 rounded border border-gray-700 bg-gray-800/30">
|
<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 justify-between mb-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Mountain className="w-4 h-4" style={{ color: elemColor }} />
|
<Mountain className="w-3 h-3" style={{ color: elemColor }} />
|
||||||
<span className="text-sm font-semibold game-panel-title" style={{ color: elemColor }}>
|
<span className="text-xs font-semibold" style={{ color: elemColor }}>{golemDef.name}</span>
|
||||||
{golemDef.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{golemDef.isAoe && (
|
{golemDef.isAoe && <span className="text-xs border border-gray-600 px-1 rounded">AOE {golemDef.aoeTargets}</span>}
|
||||||
<Badge variant="outline" className="text-xs">AOE {golemDef.aoeTargets}</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 game-mono">
|
<div className="text-xs text-gray-400">⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr</div>
|
||||||
⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr •
|
|
||||||
🗡️ {Math.floor(golemDef.armorPierce * 100)}% Pierce
|
|
||||||
</div>
|
|
||||||
{/* Attack progress bar when climbing */}
|
|
||||||
{store.currentAction === 'climb' && summoned.attackProgress > 0 && (
|
{store.currentAction === 'climb' && summoned.attackProgress > 0 && (
|
||||||
<div className="space-y-0.5 mt-1">
|
<div className="mt-1">
|
||||||
<div className="flex justify-between text-xs text-gray-500">
|
<div className="flex justify-between text-xs text-gray-500 mb-0.5">
|
||||||
<span>Attack</span>
|
<span>Attack</span>
|
||||||
<span>{Math.min(100, (summoned.attackProgress * 100)).toFixed(0)}%</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>
|
||||||
<Progress value={Math.min(100, summoned.attackProgress * 100)} className="h-1.5 bg-gray-700" />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Current Study (if any) - Only show in normal mode */}
|
{/* Guardian Panel */}
|
||||||
{!simpleMode && store.currentStudyTarget && (
|
{isGuardianFloor && simpleMode && (
|
||||||
<Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2">
|
<GuardianPanel currentFloor={store.currentFloor} floorElemDef={floorElemDef} />
|
||||||
<CardContent className="pt-4 space-y-3">
|
)}
|
||||||
<StudyProgress
|
|
||||||
currentStudyTarget={store.currentStudyTarget}
|
|
||||||
skills={store.skills}
|
|
||||||
studySpeedMult={studySpeedMult}
|
|
||||||
cancelStudy={store.cancelStudy}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Parallel Study Progress */}
|
{/* Room Display */}
|
||||||
{store.parallelStudyTarget && (
|
{simpleMode && (
|
||||||
<div className="p-3 rounded border border-cyan-600/50 bg-cyan-900/20">
|
<RoomDisplay
|
||||||
<div className="flex items-center justify-between mb-2">
|
roomType={roomType}
|
||||||
<div className="flex items-center gap-2">
|
roomConfig={roomConfig}
|
||||||
<BookOpen className="w-4 h-4 text-cyan-400" />
|
primaryEnemy={primaryEnemy}
|
||||||
<span className="text-sm font-semibold text-cyan-300">
|
swarmEnemies={swarmEnemies}
|
||||||
Parallel: {SKILLS_DEF[store.parallelStudyTarget.id]?.name}
|
puzzleId={currentRoom?.puzzleId}
|
||||||
{store.parallelStudyTarget.type === 'skill' && ` Lv.${(store.skills[store.parallelStudyTarget.id] || 0) + 1}`}
|
puzzleProgress={currentRoom?.puzzleProgress}
|
||||||
</span>
|
simpleMode={true}
|
||||||
</div>
|
floorElemDef={floorElemDef}
|
||||||
<Button
|
floorHP={store.floorHP}
|
||||||
variant="ghost"
|
floorMaxHP={store.floorMaxHP}
|
||||||
size="sm"
|
totalDPS={totalDPS}
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
currentAction={store.currentAction}
|
||||||
onClick={() => store.cancelParallelStudy?.()}
|
activeEquipmentSpells={activeEquipmentSpells}
|
||||||
>
|
/>
|
||||||
<X className="w-4 h-4" />
|
)}
|
||||||
</Button>
|
|
||||||
</div>
|
{/* Floor Controls */}
|
||||||
<Progress value={Math.min(100, (store.parallelStudyTarget.progress / store.parallelStudyTarget.required) * 100)} className="h-2 bg-gray-800" />
|
{simpleMode && (
|
||||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
<FloorControls
|
||||||
<span>{formatStudyTime(store.parallelStudyTarget.progress)} / {formatStudyTime(store.parallelStudyTarget.required)}</span>
|
store={store}
|
||||||
<span>50% speed (Parallel Study)</span>
|
climbDirection={climbDirection}
|
||||||
</div>
|
isGuardianFloor={isGuardianFloor}
|
||||||
|
currentRoom={currentRoom}
|
||||||
|
currentGuardian={currentGuardian}
|
||||||
|
isFloorCleared={isFloorCleared}
|
||||||
|
floorElemDef={floorElemDef}
|
||||||
|
roomType={roomType}
|
||||||
|
roomConfig={roomConfig}
|
||||||
|
activeEquipmentSpells={activeEquipmentSpells}
|
||||||
|
upgradeEffects={upgradeEffects}
|
||||||
|
floorElem={floorElem}
|
||||||
|
totalDPS={totalDPS}
|
||||||
|
getEnemyName={getEnemyName}
|
||||||
|
calcDamage={calcDamage}
|
||||||
|
SPELLS_DEF={SPELLS_DEF}
|
||||||
|
canCastSpell={canCastSpell}
|
||||||
|
storeCurrentAction={store.currentAction}
|
||||||
|
handleClimb={handleClimb}
|
||||||
|
formatSpellCost={formatSpellCost}
|
||||||
|
getSpellCostColor={getSpellCostColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Combat Stats Panel */}
|
||||||
|
{simpleMode && (
|
||||||
|
<CombatStatsPanel
|
||||||
|
activeEquipmentSpells={activeEquipmentSpells}
|
||||||
|
store={store}
|
||||||
|
totalDPS={totalDPS}
|
||||||
|
calcDamage={calcDamage}
|
||||||
|
formatSpellCost={formatSpellCost}
|
||||||
|
getSpellCostColor={getSpellCostColor}
|
||||||
|
SPELLS_DEF={SPELLS_DEF}
|
||||||
|
upgradeEffects={upgradeEffects}
|
||||||
|
canCastSpell={canCastSpell}
|
||||||
|
studySpeedMult={studySpeedMult}
|
||||||
|
storeCurrentAction={store.currentAction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Activity Log - Spire Mode only */}
|
||||||
|
{simpleMode && <ActivityLog activityLog={store.activityLog} />}
|
||||||
|
|
||||||
|
{/* Study Progress - Normal mode only */}
|
||||||
|
{!simpleMode && store.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(store.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, (store.currentStudyTarget.progress / store.currentStudyTarget.required) * 100)}%` }} />
|
||||||
|
</div>
|
||||||
|
{store.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(store.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, (store.parallelStudyTarget.progress / store.parallelStudyTarget.required) * 100)}%` }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Activity Log - Show in Spire Mode (simpleMode) */}
|
{/* Crafting Progress - Normal mode only */}
|
||||||
{simpleMode && (
|
{!simpleMode && (store.designProgress || store.preparationProgress || store.applicationProgress) && (
|
||||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
<Card className="bg-gray-900/80 border-cyan-600/50">
|
||||||
<CardHeader className="pb-2">
|
<CardContent className="pt-4 pb-4">
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Activity Log</CardTitle>
|
{store.designProgress && (
|
||||||
</CardHeader>
|
<div className="mb-3">
|
||||||
<CardContent>
|
<div className="text-xs text-gray-400 mb-1">Design Progress</div>
|
||||||
<ScrollArea className="h-48">
|
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||||
<div className="space-y-1">
|
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.designProgress.progress / store.designProgress.required) * 100)}%` }} />
|
||||||
{(store.activityLog || []).slice(0, 50).map((entry: ActivityLogEntry, i) => {
|
|
||||||
// Style based on event type
|
|
||||||
const getEventStyle = (eventType: 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';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={entry.id}
|
|
||||||
className={`text-xs ${i === 0 ? 'text-gray-200 font-semibold' : getEventStyle(entry.eventType)}`}
|
|
||||||
>
|
|
||||||
{entry.message}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{(store.activityLog || []).length === 0 && (
|
|
||||||
<div className="text-xs text-gray-500 italic">No activity yet...</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</div>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
{store.preparationProgress && (
|
||||||
)}
|
<div className="mb-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">Preparation Progress</div>
|
||||||
{/* Crafting Progress (if any) - Only show in normal mode */}
|
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||||
{!simpleMode && (store.designProgress || store.preparationProgress || store.applicationProgress) && (
|
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.preparationProgress.progress / store.preparationProgress.required) * 100)}%` }} />
|
||||||
<Card className="bg-gray-900/80 border-cyan-600/50 lg:col-span-2">
|
</div>
|
||||||
<CardContent className="pt-4">
|
</div>
|
||||||
<CraftingProgress
|
)}
|
||||||
designProgress={store.designProgress}
|
{store.applicationProgress && (
|
||||||
preparationProgress={store.preparationProgress}
|
<div>
|
||||||
applicationProgress={store.applicationProgress}
|
<div className="text-xs text-gray-400 mb-1">Application Progress</div>
|
||||||
equipmentInstances={store.equipmentInstances}
|
<div className="h-2 bg-gray-800 rounded-full overflow-hidden">
|
||||||
enchantmentDesigns={store.enchantmentDesigns}
|
<div className="h-full rounded-full transition-all duration-300 bg-cyan-500" style={{ width: `${Math.min(100, (store.applicationProgress.progress / store.applicationProgress.required) * 100)}%` }} />
|
||||||
cancelDesign={store.cancelDesign!}
|
</div>
|
||||||
cancelPreparation={store.cancelPreparation!}
|
</div>
|
||||||
pauseApplication={store.pauseApplication!}
|
)}
|
||||||
resumeApplication={store.resumeApplication!}
|
</CardContent>
|
||||||
cancelApplication={store.cancelApplication!}
|
</Card>
|
||||||
/>
|
)}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SpireTab.displayName = "SpireTab";
|
SpireTab.displayName = "SpireTab";
|
||||||
|
|
||||||
|
function getSkillName(skillId: string): string {
|
||||||
|
const { SKILLS_DEF } = require('@/lib/game/constants');
|
||||||
|
return SKILLS_DEF[skillId]?.name || skillId;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getGolemDef(golemId: string) {
|
||||||
|
const { GOLEMS_DEF } = require('@/lib/game/data/golems');
|
||||||
|
return GOLEMS_DEF[golemId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGolemDamage(golemId: string, skills: any) {
|
||||||
|
const { getGolemDamage } = require('@/lib/game/data/golems');
|
||||||
|
return getGolemDamage(golemId, skills);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGolemAttackSpeed(golemId: string, skills: any) {
|
||||||
|
const { getGolemAttackSpeed } = require('@/lib/game/data/golems');
|
||||||
|
return getGolemAttackSpeed(golemId, skills);
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,3 +13,11 @@ export { DebugTab } from './DebugTab';
|
|||||||
export { LootTab } from './LootTab';
|
export { LootTab } from './LootTab';
|
||||||
export { AchievementsTab } from './AchievementsTab';
|
export { AchievementsTab } from './AchievementsTab';
|
||||||
export { GolemancyTab } from './GolemancyTab';
|
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';
|
||||||
|
|||||||
+63
-13
@@ -1,14 +1,64 @@
|
|||||||
// ─── Game Types ───────────────────────────────────────────────────────────────
|
import type { GameStore } from '@/lib/game/store';
|
||||||
// This file now re-exports types from domain-specific files in the types/ directory.
|
import type { SPELLS } from '@/lib/game/constants';
|
||||||
// All type definitions have been moved to:
|
import type { EquipmentSpellState } from '@/lib/game/state';
|
||||||
// - types/elements.ts - element-related types
|
import type { RoomType, ActivityLogEntry } from '@/lib/game/types/game';
|
||||||
// - types/attunements.ts - attunement-related types
|
|
||||||
// - types/spells.ts - spell-related types
|
|
||||||
// - types/skills.ts - skill-related types
|
|
||||||
// - types/equipment.ts - equipment-related types
|
|
||||||
// - types/game.ts - core game state types
|
|
||||||
//
|
|
||||||
// Import from this file (types.ts) to maintain backward compatibility,
|
|
||||||
// or import directly from the domain-specific files.
|
|
||||||
|
|
||||||
export * from './types/index';
|
// Re-export ActivityLogEntry for convenience
|
||||||
|
export { ActivityLogEntry };
|
||||||
|
|
||||||
|
// Room Display Props
|
||||||
|
export interface RoomDisplayProps {
|
||||||
|
roomType: RoomType;
|
||||||
|
roomConfig: { label: string; icon: string; color: string };
|
||||||
|
primaryEnemy: any;
|
||||||
|
swarmEnemies: any[];
|
||||||
|
puzzleId?: string;
|
||||||
|
puzzleProgress?: number;
|
||||||
|
simpleMode: boolean;
|
||||||
|
floorElemDef: any;
|
||||||
|
floorHP: number;
|
||||||
|
floorMaxHP: number;
|
||||||
|
totalDPS: number;
|
||||||
|
currentAction: string | null;
|
||||||
|
activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floor Controls Props
|
||||||
|
export interface FloorControlsProps {
|
||||||
|
store: GameStore;
|
||||||
|
climbDirection: 'up' | 'down' | null;
|
||||||
|
isGuardianFloor: boolean;
|
||||||
|
currentRoom: any;
|
||||||
|
currentGuardian: any;
|
||||||
|
isFloorCleared: boolean;
|
||||||
|
floorElemDef: any;
|
||||||
|
roomType: RoomType;
|
||||||
|
roomConfig: { label: string; icon: string; color: string };
|
||||||
|
activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>;
|
||||||
|
upgradeEffects: any;
|
||||||
|
floorElem: string;
|
||||||
|
totalDPS: number;
|
||||||
|
getEnemyName: typeof import('@/lib/game/store').getEnemyName;
|
||||||
|
calcDamage: typeof import('@/lib/game/store').calcDamage;
|
||||||
|
SPELLS_DEF: typeof import('@/lib/game/constants').SPELLS_DEF;
|
||||||
|
canCastSpell: (spellId: string) => boolean;
|
||||||
|
storeCurrentAction: string | null;
|
||||||
|
handleClimb: (direction: 'up' | 'down') => void;
|
||||||
|
formatSpellCost: typeof import('@/lib/game/formatting').formatSpellCost;
|
||||||
|
getSpellCostColor: typeof import('@/lib/game/formatting').getSpellCostColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combat Stats Panel Props
|
||||||
|
export interface CombatStatsPanelProps {
|
||||||
|
activeEquipmentSpells: Array<{ spellId: string; equipmentId: string }>;
|
||||||
|
store: GameStore;
|
||||||
|
totalDPS: number;
|
||||||
|
calcDamage: typeof import('@/lib/game/store').calcDamage;
|
||||||
|
formatSpellCost: typeof import('@/lib/game/formatting').formatSpellCost;
|
||||||
|
getSpellCostColor: typeof import('@/lib/game/formatting').getSpellCostColor;
|
||||||
|
SPELLS_DEF: typeof import('@/lib/game/constants').SPELLS_DEF;
|
||||||
|
upgradeEffects: any;
|
||||||
|
canCastSpell: (spellId: string) => boolean;
|
||||||
|
studySpeedMult: number;
|
||||||
|
storeCurrentAction: string | null;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user