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,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);
|
||||
}
|
||||
Reference in New Issue
Block a user