Fix Task6: Remove duplicate SpireTab.tsx and verify swarm mode
- Deleted old src/components/game/SpireTab.tsx (duplicate of tabs/SpireTab.tsx) - Verified SWARM_CONFIG: 15% chance, 3-6 enemies at 40% HP - Verified UI in tabs/SpireTab.tsx correctly renders each swarm enemy individually - Verified generateSwarmEnemies() and generateRoomType() logic is correct
This commit is contained in:
@@ -1,486 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useGameStore, canAffordSpellCost, fmt, fmtDec, calcDamage, getEnemyName } from '@/lib/game/store';
|
|
||||||
import type { ActivityLogEntry } from '@/lib/game/types';
|
|
||||||
import { ELEMENTS, GUARDIANS, SPELLS_DEF, ROOM_TYPE_LABELS } from '@/lib/game/constants';
|
|
||||||
import { useManaStats, useCombatStats, useStudyStats } from '@/lib/game/hooks/useGameDerived';
|
|
||||||
import { formatSpellCost, getSpellCostColor, formatStudyTime } from '@/lib/game/formatting';
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { Progress } from '@/components/ui/progress';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
||||||
import { Separator } from '@/components/ui/separator';
|
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
||||||
import { X, BookOpen, Skull, Shield, Wind } from 'lucide-react';
|
|
||||||
|
|
||||||
export function SpireTab() {
|
|
||||||
const store = useGameStore();
|
|
||||||
const { effectiveRegen, meditationMultiplier, incursionStrength } = useManaStats();
|
|
||||||
const {
|
|
||||||
floorElem, floorElemDef, isGuardianFloor, currentGuardian,
|
|
||||||
activeSpellDef, dps, damageBreakdown
|
|
||||||
} = useCombatStats();
|
|
||||||
const { effectiveStudySpeedMult } = useStudyStats();
|
|
||||||
|
|
||||||
// Get room type info
|
|
||||||
const currentRoom = store.currentRoom;
|
|
||||||
const roomType = currentRoom?.roomType || 'combat';
|
|
||||||
const roomConfig = ROOM_TYPE_LABELS[roomType] || ROOM_TYPE_LABELS.combat;
|
|
||||||
|
|
||||||
// Check if spell can be cast
|
|
||||||
const canCastSpell = (spellId: string): boolean => {
|
|
||||||
const spell = SPELLS_DEF[spellId];
|
|
||||||
if (!spell) return false;
|
|
||||||
return canAffordSpellCost(spell.cost, store.rawMana, store.elements);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get enemy display info
|
|
||||||
const getEnemyDisplayInfo = () => {
|
|
||||||
if (!currentRoom || !currentRoom.enemies || currentRoom.enemies.length === 0) {
|
|
||||||
return { primaryEnemy: null, swarmEnemies: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Render study progress
|
|
||||||
const renderStudyProgress = () => {
|
|
||||||
if (!store.currentStudyTarget) return null;
|
|
||||||
|
|
||||||
const target = store.currentStudyTarget;
|
|
||||||
const progressPct = Math.min(100, (target.progress / target.required) * 100);
|
|
||||||
const isSkill = target.type === 'skill';
|
|
||||||
const def = isSkill ? SPELLS_DEF[target.id] : SPELLS_DEF[target.id];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-3 rounded border border-purple-600/50 bg-purple-900/20">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<BookOpen className="w-4 h-4 text-purple-400" />
|
|
||||||
<span className="text-sm font-semibold text-purple-300">
|
|
||||||
{def?.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={() => store.cancelStudy()}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Progress value={progressPct} className="h-2 bg-gray-800" />
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
||||||
<span>{formatStudyTime(target.progress)} / {formatStudyTime(target.required)}</span>
|
|
||||||
<span>{effectiveStudySpeedMult.toFixed(1)}x speed</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
||||||
{/* Current Floor Card */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs flex items-center justify-between">
|
|
||||||
<span>Current Floor</span>
|
|
||||||
<Badge
|
|
||||||
className="ml-2"
|
|
||||||
style={{
|
|
||||||
backgroundColor: `${roomConfig.color}20`,
|
|
||||||
color: roomConfig.color,
|
|
||||||
borderColor: `${roomConfig.color}60`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{roomConfig.icon} {roomConfig.label}
|
|
||||||
</Badge>
|
|
||||||
</CardTitle>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
{isGuardianFloor && currentGuardian && (
|
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</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 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>
|
|
||||||
</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' && canCastSpell(store.activeSpell) ? fmtDec(dps) : '—'}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Separator className="bg-gray-700" />
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Active Spell Card */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Active Spell</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3">
|
|
||||||
{activeSpellDef ? (
|
|
||||||
<>
|
|
||||||
<div className="text-lg font-semibold game-panel-title" style={{ color: activeSpellDef.elem === 'raw' ? '#60A5FA' : ELEMENTS[activeSpellDef.elem]?.color }}>
|
|
||||||
{activeSpellDef.name}
|
|
||||||
{activeSpellDef.tier === 0 && <Badge className="ml-2 bg-gray-600 text-gray-200">Basic</Badge>}
|
|
||||||
{activeSpellDef.tier >= 4 && <Badge className="ml-2 bg-amber-600 text-amber-100">Legendary</Badge>}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-400 game-mono">
|
|
||||||
⚔️ {fmt(calcDamage(store, store.activeSpell))} dmg •
|
|
||||||
<span style={{ color: getSpellCostColor(activeSpellDef.cost) }}>
|
|
||||||
{' '}{formatSpellCost(activeSpellDef.cost)}
|
|
||||||
</span>
|
|
||||||
{' '}• ⚡ {(activeSpellDef.castSpeed || 1).toFixed(1)} casts/hr
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cast progress bar when climbing */}
|
|
||||||
{store.currentAction === 'climb' && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="flex justify-between text-xs text-gray-400">
|
|
||||||
<span>Cast Progress</span>
|
|
||||||
<span>{((store.castProgress || 0) * 100).toFixed(0)}%</span>
|
|
||||||
</div>
|
|
||||||
<Progress value={Math.min(100, (store.castProgress || 0) * 100)} className="h-2 bg-gray-800" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSpellDef.desc && (
|
|
||||||
<div className="text-xs text-gray-500 italic">{activeSpellDef.desc}</div>
|
|
||||||
)}
|
|
||||||
{activeSpellDef.effects && activeSpellDef.effects.length > 0 && (
|
|
||||||
<div className="flex gap-1 flex-wrap">
|
|
||||||
{activeSpellDef.effects.map((eff, i) => (
|
|
||||||
<Badge key={i} variant="outline" className="text-xs">
|
|
||||||
{eff.type === 'burn' && `🔥 Burn ${eff.value}/hr`}
|
|
||||||
{eff.type === 'stun' && `⚡ Stun ${eff.value}s`}
|
|
||||||
{eff.type === 'pierce' && `🗡️ Pierce ${Math.round(eff.value * 100)}%`}
|
|
||||||
{eff.type === 'multicast' && `✨ ${Math.round(eff.value * 100)}% Multicast`}
|
|
||||||
{eff.type === 'buff' && `💪 Buff`}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-gray-500">No spell selected</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Can cast indicator */}
|
|
||||||
{activeSpellDef && (
|
|
||||||
<div className={`text-xs ${canCastSpell(store.activeSpell) ? 'text-green-400' : 'text-red-400'}`}>
|
|
||||||
{canCastSpell(store.activeSpell) ? '✓ Can cast' : '✗ Insufficient mana'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{incursionStrength > 0 && (
|
|
||||||
<div className="p-2 bg-red-900/20 border border-red-800/50 rounded">
|
|
||||||
<div className="text-xs text-red-400 game-panel-title mb-1">LABYRINTH INCURSION</div>
|
|
||||||
<div className="text-sm text-gray-300">
|
|
||||||
-{Math.round(incursionStrength * 100)}% mana regen
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Current Study (if any) */}
|
|
||||||
{store.currentStudyTarget && (
|
|
||||||
<Card className="bg-gray-900/80 border-purple-600/50 lg:col-span-2">
|
|
||||||
<CardContent className="pt-4 space-y-3">
|
|
||||||
{renderStudyProgress()}
|
|
||||||
|
|
||||||
{/* Parallel Study Progress */}
|
|
||||||
{store.parallelStudyTarget && (
|
|
||||||
<div className="p-3 rounded border border-cyan-600/50 bg-cyan-900/20">
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<BookOpen className="w-4 h-4 text-cyan-400" />
|
|
||||||
<span className="text-sm font-semibold text-cyan-300">
|
|
||||||
Parallel: {store.parallelStudyTarget.type === 'skill' ? store.parallelStudyTarget.id : store.parallelStudyTarget.id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-6 w-6 p-0 text-gray-400 hover:text-white"
|
|
||||||
onClick={() => store.cancelParallelStudy()}
|
|
||||||
>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Progress value={Math.min(100, (store.parallelStudyTarget.progress / store.parallelStudyTarget.required) * 100)} className="h-2 bg-gray-800" />
|
|
||||||
<div className="flex justify-between text-xs text-gray-400 mt-1">
|
|
||||||
<span>{formatStudyTime(store.parallelStudyTarget.progress)} / {formatStudyTime(store.parallelStudyTarget.required)}</span>
|
|
||||||
<span>50% speed (Parallel Study)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pact Signing Progress */}
|
|
||||||
|
|
||||||
|
|
||||||
{/* Spells Available */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<CardTitle className="text-amber-400 game-panel-title text-xs">Known Spells</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2">
|
|
||||||
{Object.entries(store.spells)
|
|
||||||
.filter(([, state]) => state.learned)
|
|
||||||
.map(([id, state]) => {
|
|
||||||
const def = SPELLS_DEF[id];
|
|
||||||
if (!def) return null;
|
|
||||||
const elemDef = def.elem === 'raw' ? null : ELEMENTS[def.elem];
|
|
||||||
const isActive = store.activeSpell === id;
|
|
||||||
const canCast = canCastSpell(id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={id}
|
|
||||||
variant="outline"
|
|
||||||
className={`h-auto py-2 px-3 flex flex-col items-start ${isActive ? 'border-amber-500 bg-amber-900/20' : canCast ? 'border-gray-600 bg-gray-800/50 hover:bg-gray-700/50' : 'border-gray-700 bg-gray-800/30 opacity-60'}`}
|
|
||||||
onClick={() => store.setSpell(id)}
|
|
||||||
>
|
|
||||||
<div className="text-sm font-semibold" style={{ color: def.elem === 'raw' ? '#60A5FA' : elemDef?.color }}>
|
|
||||||
{def.name}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-400 game-mono">
|
|
||||||
{fmt(calcDamage(store, id))} dmg
|
|
||||||
</div>
|
|
||||||
<div className="text-xs game-mono" style={{ color: getSpellCostColor(def.cost) }}>
|
|
||||||
{formatSpellCost(def.cost)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Activity Log */}
|
|
||||||
<Card className="bg-gray-900/80 border-gray-700 lg:col-span-2">
|
|
||||||
<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">
|
|
||||||
{(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>
|
|
||||||
</ScrollArea>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
SpireTab.displayName = "SpireTab";
|
|
||||||
@@ -353,11 +353,11 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-400 game-mono mb-1">
|
<div className="text-xs text-gray-400 game-mono mb-1">
|
||||||
⚔️ {fmt(totalDPS)} DPS •
|
⚔️ {fmt(calcDamage(store, spellId))} dmg/cast •
|
||||||
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
<span style={{ color: getSpellCostColor(spellDef.cost) }}>
|
||||||
{' '}{formatSpellCost(spellDef.cost)}
|
{' '}{formatSpellCost(spellDef.cost)}
|
||||||
</span>
|
</span>
|
||||||
{' '}• ⚡ {(spellDef.castSpeed || 1).toFixed(1)}/hr
|
{' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cast progress bar when climbing */}
|
{/* Cast progress bar when climbing */}
|
||||||
|
|||||||
@@ -847,8 +847,8 @@ function addActivityLogEntry(
|
|||||||
details?: ActivityLogEntry['details']
|
details?: ActivityLogEntry['details']
|
||||||
): ActivityLogEntry[] {
|
): ActivityLogEntry[] {
|
||||||
const entry = createActivityEntry(eventType, message, details);
|
const entry = createActivityEntry(eventType, message, details);
|
||||||
// Keep last 100 entries, newest first
|
// Keep last 50 entries, newest first (Task 10)
|
||||||
return [entry, ...state.activityLog.slice(0, 99)];
|
return [entry, ...state.activityLog.slice(0, 49)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Game Store ───────────────────────────────────────────────────────────────
|
// ─── Game Store ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user