From dc38445225481c824360e8bc144e7d8e0b16dc28 Mon Sep 17 00:00:00 2001 From: Refactoring Agent <[email protected]> Date: Fri, 1 May 2026 17:02:32 +0200 Subject: [PATCH] refactor: extract components from SpireTab.tsx to reduce below 400 lines --- src/components/game/tabs/ActivityLog.tsx | 68 ++ src/components/game/tabs/CombatStatsPanel.tsx | 164 ++++ src/components/game/tabs/FloorControls.tsx | 191 ++++ src/components/game/tabs/GuardianPanel.tsx | 53 ++ src/components/game/tabs/RoomDisplay.tsx | 194 ++++ src/components/game/tabs/SpireHeader.tsx | 109 +++ src/components/game/tabs/SpireTab.tsx | 849 ++++++------------ src/components/game/tabs/index.ts | 8 + src/lib/game/types.ts | 76 +- 9 files changed, 1148 insertions(+), 564 deletions(-) create mode 100644 src/components/game/tabs/ActivityLog.tsx create mode 100644 src/components/game/tabs/CombatStatsPanel.tsx create mode 100644 src/components/game/tabs/FloorControls.tsx create mode 100644 src/components/game/tabs/GuardianPanel.tsx create mode 100644 src/components/game/tabs/RoomDisplay.tsx create mode 100644 src/components/game/tabs/SpireHeader.tsx diff --git a/src/components/game/tabs/ActivityLog.tsx b/src/components/game/tabs/ActivityLog.tsx new file mode 100644 index 0000000..464e8c8 --- /dev/null +++ b/src/components/game/tabs/ActivityLog.tsx @@ -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 ( + + + Activity Log + + + +
+ {entries.slice(0, 50).map((entry, i) => { + const isLatest = i === 0; + const color = getEventStyle(entry.eventType); + return ( +
+ {entry.message} +
+ ); + })} + {entries.length === 0 && ( +
No activity yet...
+ )} +
+
+
+
+ ); +} + +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'; + } +} diff --git a/src/components/game/tabs/CombatStatsPanel.tsx b/src/components/game/tabs/CombatStatsPanel.tsx new file mode 100644 index 0000000..34ae74f --- /dev/null +++ b/src/components/game/tabs/CombatStatsPanel.tsx @@ -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 ( + + + Combat Stats + + +
+ Total DPS: {storeCurrentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'} +
+ + {activeEquipmentSpells.length > 0 && ( +
+
Active Spells
+ {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 ( +
+
+ + {spellDef.name} + {spellDef.tier === 0 && Basic} + {spellDef.tier >= 4 && Legendary} + + + {canCast ? '✓' : '✗'} + +
+
+ ⚔️ {fmt(calcDamage(store, spellId))} dmg • {' '} + + {formatSpellCost(spellDef.cost)} + + {' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr +
+ {storeCurrentAction === 'climb' && ( +
+
+ Cast + {(progress * 100).toFixed(0)}% +
+
+
+
+
+ )} +
+ ); + })} +
+ )} + + {activeGolems.length > 0 && ( +
+
+ + Active Golems +
+ {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 ( +
+
+
+ + + {golemDef.name} + +
+ {golemDef.isAoe && ( + AOE {golemDef.aoeTargets} + )} +
+
+ ⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr +
+ {storeCurrentAction === 'climb' && summoned.attackProgress > 0 && ( +
+
+ Attack + {Math.min(100, (summoned.attackProgress * 100)).toFixed(0)}% +
+
+
+
+
+ )} +
+ ); + })} +
+ )} + +
+
Study Speed: {Math.round(studySpeedMult * 100)}%
+
+ + + ); +} + +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); +} diff --git a/src/components/game/tabs/FloorControls.tsx b/src/components/game/tabs/FloorControls.tsx new file mode 100644 index 0000000..3b4e69e --- /dev/null +++ b/src/components/game/tabs/FloorControls.tsx @@ -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 = { + 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 ( + + + Floor Navigation + + +
+ + +
+ + {storeCurrentAction === 'climb' && ( +
+
+ + + Climbing {climbDirection === 'up' ? 'Up' : 'Down'} + + {isGuardianFloor && ( + GUARDIAN + )} +
+ + {currentGuardian && ( +
+
+ + + {currentGuardian.name} + +
+
+ )} + + {!(roomType === 'swarm' || roomType === 'puzzle') && ( +
+
+
+
+
+ {fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP + + DPS: {activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'} + +
+
+ )} + + {activeEquipmentSpells.length > 0 ? ( +
+ {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 ( +
+
+ + {spellDef.name} + + + {canCast ? '✓' : '✗'} + +
+
+ ⚔️ {fmt(calcDamage(store, spellId))} dmg • {' '} + + {formatSpellCost(spellDef.cost)} + +
+
+
+ Cast + {(progress * 100).toFixed(0)}% +
+
+
+
+
+
+ ); + })} +
+ ) : ( +
No active spells. Equip staves with spell effects.
+ )} +
+ )} + + {storeCurrentAction !== 'climb' && ( +
+ Click Climb Up/Down to begin climbing +
+ )} + + + ); +} + +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); +} diff --git a/src/components/game/tabs/GuardianPanel.tsx b/src/components/game/tabs/GuardianPanel.tsx new file mode 100644 index 0000000..993777d --- /dev/null +++ b/src/components/game/tabs/GuardianPanel.tsx @@ -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 ( +
+
+ ⚔️ + + {guardian.name} + +
+ +
+
Power: {fmt(guardian.power)}
+ + {guardian.armor > 0 && ( +
Armor: {(guardian.armor * 100).toFixed(0)}%
+ )} + + {guardian.effects && guardian.effects.length > 0 && ( +
+ {guardian.effects.map((eff: any, i: number) => ( + + {eff.type === 'burn' && `🔥 Burn ${(eff.value * 100).toFixed(0)}%`} + {eff.type === 'armor_pierce' && `🗡️ Pierce ${(eff.value * 100).toFixed(0)}%`} + + ))} +
+ )} +
+
+ ); +} + +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); +} diff --git a/src/components/game/tabs/RoomDisplay.tsx b/src/components/game/tabs/RoomDisplay.tsx new file mode 100644 index 0000000..292b16d --- /dev/null +++ b/src/components/game/tabs/RoomDisplay.tsx @@ -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 = { + 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 ( +
+
+ 🧩 + + {puzzleId ? puzzleId.replace(/_/g, ' ').toUpperCase() : 'Puzzle Room'} + +
+
+
+ Progress + {((puzzleProgress || 0) * 100).toFixed(0)}% +
+ +
+
+ ); + } + + // Single Enemy Display (Combat/Speed/Guardian - non-swarm) + if ((roomType === 'combat' || roomType === 'speed' || roomType === 'guardian') && primaryEnemy) { + return ( +
+
+
+ + + {primaryEnemy.name || 'Unknown Enemy'} + +
+ + {ELEMENTS[primaryEnemy.element]?.sym} {ELEMENTS[primaryEnemy.element]?.name} + +
+ + {/* Enemy HP Bar */} +
+
+
+
+
+ {fmt(primaryEnemy.hp)} / {fmt(primaryEnemy.maxHP)} HP +
+
+ + {/* Enemy Properties */} + +
+ ); + } + + // Swarm Enemies Display + if (roomType === 'swarm' && swarmEnemies && swarmEnemies.length > 0) { + return ( +
+
+ Swarm Enemies ({swarmEnemies.length}) +
+ {swarmEnemies.map((enemy: any, index: number) => ( +
+
+
+ + + {enemy.name || `Enemy ${index + 1}`} + +
+ + {ELEMENTS[enemy.element]?.sym} {fmt(enemy.hp)}/{fmt(enemy.maxHP)} HP + +
+
+
+
+ +
+ ))} +
+ ); + } + + // Floor HP Bar (non-swarm, non-puzzle) + return ( +
+
+
+
+
+ {fmt(floorHP)} / {fmt(floorMaxHP)} HP + DPS: {currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'} +
+
+ ); +} + +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 ( +
+ {props.map((p, i) => { + const Icon = p.icon; + return ( + + + + + {p.label} + + + +

Reduces incoming damage by {(enemy.armor * 100).toFixed(0)}%

+
+
+ ); + })} +
+ ); +} + +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); +} diff --git a/src/components/game/tabs/SpireHeader.tsx b/src/components/game/tabs/SpireHeader.tsx new file mode 100644 index 0000000..9bbfa16 --- /dev/null +++ b/src/components/game/tabs/SpireHeader.tsx @@ -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 && ( +
+
+

+ Current Floor + + {roomIcon} {roomLabel} + +

+
+ +
+
+ + {currentFloor} + + / 100 + + {floorElemDef?.sym} {floorElemDef?.name} + + {isGuardianFloor && ( + GUARDIAN + )} +
+ + {isGuardianFloor && ( +
+ ⚔️ Guardian Floor +
+ )} + +
+ Best: Floor {maxFloorReached} • + Pacts: {signedPacts} +
+
+
+ )} + + {/* Spire Stats Card - Only show in normal mode */} + {!simpleMode && ( +
+
+

Spire Stats

+
+ +
+
+
{maxFloorReached}
+
Best Floor
+
+
+
{signedPacts}
+
Pacts Signed
+
+
+ +
+ Current Floor: {currentFloor} +
+
+ )} + + ); +} diff --git a/src/components/game/tabs/SpireTab.tsx b/src/components/game/tabs/SpireTab.tsx index af312dd..76abc24 100755 --- a/src/components/game/tabs/SpireTab.tsx +++ b/src/components/game/tabs/SpireTab.tsx @@ -1,34 +1,26 @@ 'use client'; import { Button } from '@/components/ui/button'; -import { Progress } from '@/components/ui/progress'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -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 { Card, CardContent } from '@/components/ui/card'; +import { Mountain } from 'lucide-react'; import type { ActivityLogEntry } from '@/lib/game/types'; import type { GameStore } from '@/lib/game/store'; -import { ELEMENTS, GUARDIANS, SPELLS_DEF, SKILLS_DEF, ROOM_TYPE_LABELS } from '@/lib/game/constants'; -import { GOLEMS_DEF, getGolemDamage, getGolemAttackSpeed } from '@/lib/game/data/golems'; -import { fmt, fmtDec, getFloorElement, canAffordSpellCost, getEnemyName, calcDamage } from '@/lib/game/store'; +import { ELEMENTS, GUARDIANS, SPELLS_DEF } from '@/lib/game/constants'; +import { getEnemyName, calcDamage } from '@/lib/game/store'; 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 { formatSpellCost, getSpellCostColor } from '@/lib/game/formatting'; +import { canAffordSpellCost, getFloorElement } from '@/lib/game/store'; -interface SpireTabProps { - store: GameStore; - simpleMode?: boolean; // When true, only show essential Spire info (for Spire Mode) -} +// Extracted components +import { SpireHeader } from './SpireHeader'; +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 -const canEnterSpireMode = (store: GameStore): boolean => { - return !store.spireMode; // Can enter if not already in Spire Mode -}; - -// Room type configurations for display +// Room type configurations const ROOM_TYPE_CONFIG: Record = { combat: { label: 'Combat', icon: '⚔️', color: '#EF4444' }, swarm: { label: 'Swarm', icon: '🐝', color: '#F59E0B' }, @@ -37,7 +29,18 @@ const ROOM_TYPE_CONFIG: Record { + return !store.spireMode; +}; + export function SpireTab({ store, simpleMode = false }: SpireTabProps) { + // Derived data const floorElem = getFloorElement(store.currentFloor); const floorElemDef = ELEMENTS[floorElem]; const isGuardianFloor = !!GUARDIANS[store.currentFloor]; @@ -45,576 +48,320 @@ export function SpireTab({ store, simpleMode = false }: SpireTabProps) { const climbDirection = store.climbDirection || 'up'; const clearedFloors = store.clearedFloors || {}; const currentRoom = store.currentRoom; - - // Check if current floor is cleared (for respawn indicator) const isFloorCleared = clearedFloors[store.currentFloor]; - - // Get room type info const roomType = currentRoom?.roomType || 'combat'; const roomConfig = ROOM_TYPE_CONFIG[roomType] || ROOM_TYPE_CONFIG.combat; - - // Get active equipment spells const activeEquipmentSpells = getActiveEquipmentSpells(store.equippedInstances, store.equipmentInstances); - - // Get upgrade effects and DPS const upgradeEffects = getUnifiedEffects(store); 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 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: [] }; + // Climb handler + const handleClimb = (direction: 'up' | 'down') => { + if (direction === 'up') { + 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 ( - -
- {/* Spire Stats View - Only show in normal mode (not simpleMode) */} - {!simpleMode && ( - <> - {/* Enter Spire Mode Button */} - - - -
- Climb the Spire to face guardians and earn pacts -
-
-
- - {/* Spire Stats Card - Replaces Current Floor stat */} - - - Spire Stats - - -
-
-
{store.maxFloorReached}
-
Best Floor
-
-
-
{store.signedPacts.length}
-
Pacts Signed
-
-
-
- Current Floor: {store.currentFloor} -
-
-
- - )} - - {/* Current Floor Card - Only show in Spire Mode (simpleMode) */} - {simpleMode && ( - - - - Current Floor - - {roomConfig.icon} {roomConfig.label} - - - - -
- - {store.currentFloor} - - / 100 - - {floorElemDef?.sym} {floorElemDef?.name} - - {isGuardianFloor && ( - GUARDIAN - )} -
- - {/* Guardian Name */} - {isGuardianFloor && currentGuardian && ( -
- ⚔️ {currentGuardian.name} -
- )} - - {/* Single Enemy Display (Combat/Speed/Guardian) */} - {!isGuardianFloor && primaryEnemy && roomType !== 'swarm' && ( -
-
-
- - - {primaryEnemy.name || 'Unknown Enemy'} - -
- - {ELEMENTS[primaryEnemy.element]?.sym} {ELEMENTS[primaryEnemy.element]?.name} - -
- - {/* Enemy HP Bar */} -
-
-
-
-
- {fmt(primaryEnemy.hp)} / {fmt(primaryEnemy.maxHP)} HP -
-
- - {/* Enemy Properties */} -
- {primaryEnemy.armor > 0 && ( - - - - - {(primaryEnemy.armor * 100).toFixed(0)}% Armor - - - -

Reduces incoming damage by {(primaryEnemy.armor * 100).toFixed(0)}%

-
-
- )} - {primaryEnemy.dodgeChance > 0 && ( - - - - - {(primaryEnemy.dodgeChance * 100).toFixed(0)}% Dodge - - - -

Chance to dodge attacks and reduce progress

-
-
- )} - {primaryEnemy.healthRegen > 0 && ( - - - - - {(primaryEnemy.healthRegen * 100).toFixed(1)}% Regen - - - -

Regenerates {(primaryEnemy.healthRegen * 100).toFixed(1)}% of max HP per tick

-
-
- )} - {primaryEnemy.barrier > 0 && ( - - - - - {(primaryEnemy.barrier * 100).toFixed(0)}% Barrier - - - -

Has a barrier absorbing {(primaryEnemy.barrier * 100).toFixed(0)}% of max HP before HP takes damage

-
-
- )} -
-
- )} - - {/* Swarm Enemies Display */} - {roomType === 'swarm' && swarmEnemies.length > 0 && ( -
-
- Swarm Enemies ({swarmEnemies.length}) -
- {swarmEnemies.map((enemy, index) => ( -
+
+ {/* Enter Spire Mode - Normal mode only */} + {!simpleMode && ( + + + +
+ Climb the Spire to face guardians and earn pacts +
+
+
+ )} + + {/* Spire Header */} + + + {/* Active Spells Card - Spire Mode only */} + {simpleMode && ( + + +
+ Active Spells ({activeEquipmentSpells.length}) +
+ {activeEquipmentSpells.length > 0 ? ( +
+ {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 ( +
-
- - - {enemy.name || `Enemy ${index + 1}`} - + + {spellDef.name} + {spellDef.tier === 0 && Basic} + + {canCast ? '✓' : '✗'} +
+
+ ⚔️ {fmt(calcDamage(store, spellId))} dmg • {formatSpellCost(spellDef.cost)} {' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr +
+ {store.currentAction === 'climb' && ( +
+
+ Cast + {(progress * 100).toFixed(0)}% +
+
+
+
- - {ELEMENTS[enemy.element]?.sym} {fmt(enemy.hp)}/{fmt(enemy.maxHP)} HP - -
-
-
-
- {/* Show enemy properties for swarm enemies */} -
- {enemy.armor > 0 && ( - - - {(enemy.armor * 100).toFixed(0)}% Armor - - )} - {enemy.healthRegen > 0 && ( - - - {(enemy.healthRegen * 100).toFixed(1)}% Regen - - )} - {enemy.barrier > 0 && ( - - - {(enemy.barrier * 100).toFixed(0)}% Barrier - - )} -
+ )}
- ))} -
- )} - - {/* Puzzle Room Display */} - {roomType === 'puzzle' && ( -
-
- 🧩 - - {currentRoom.puzzleId ? currentRoom.puzzleId.replace(/_/g, ' ').toUpperCase() : 'Puzzle Room'} - -
-
-
- Progress - {((currentRoom.puzzleProgress || 0) * 100).toFixed(0)}% -
- -
-
- )} - - {/* Floor HP Bar (for non-swarm, non-puzzle) */} - {roomType !== 'swarm' && roomType !== 'puzzle' && ( -
-
-
-
-
- {fmt(store.floorHP)} / {fmt(store.floorMaxHP)} HP - DPS: {store.currentAction === 'climb' && activeEquipmentSpells.length > 0 ? fmtDec(totalDPS) : '—'} -
-
- )} - -
- Best: Floor {store.maxFloorReached} • - Pacts: {store.signedPacts.length} + ); + })}
- - - )} - - {/* Active Spells Card - Only show in Spire Mode (simpleMode) */} - {simpleMode && ( - - - - Active Spells ({activeEquipmentSpells.length}) - - - - {activeEquipmentSpells.length > 0 ? ( -
- {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 ( -
-
-
- {spellDef.name} - {spellDef.tier === 0 && Basic} - {spellDef.tier >= 4 && Legendary} -
- - {canCast ? '✓' : '✗'} - -
-
- ⚔️ {fmt(calcDamage(store, spellId))} dmg/cast • - - {' '}{formatSpellCost(spellDef.cost)} - - {' '}• ⚡ {fmt(Math.floor(calcDamage(store, spellId) * (spellDef.castSpeed || 1)))} dmg/hr -
- - {/* Cast progress bar when climbing */} - {store.currentAction === 'climb' && ( -
-
- Cast - {(progress * 100).toFixed(0)}% -
- -
- )} - - {spellDef.effects && spellDef.effects.length > 0 && ( -
- {spellDef.effects.map((eff, i) => ( - - {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`} - - ))} -
- )} -
- ); - })} -
- ) : ( -
No spells on equipped weapons. Enchant a staff with spell effects in the Crafting tab.
- )} -
-
- )} - - {/* Summoned Golems Card - Show in Spire Mode (simpleMode) or if have golems */} - {simpleMode && store.golemancy.summonedGolems.length > 0 && ( - - - - - Active Golems ({store.golemancy.summonedGolems.length}) - - - + ) : ( +
No spells on equipped weapons. Enchant a staff with spell effects in the Crafting tab.
+ )} +
+
+ )} + + {/* Summoned Golems */} + {simpleMode && store.golemancy.summonedGolems.length > 0 && ( + + +
+ + Active Golems ({store.golemancy.summonedGolems.length}) +
+
{store.golemancy.summonedGolems.map((summoned) => { - const golemDef = GOLEMS_DEF[summoned.golemId]; + const golemDef = getGolemDef(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 ( -
+
- - - {golemDef.name} - + + {golemDef.name}
- {golemDef.isAoe && ( - AOE {golemDef.aoeTargets} - )} + {golemDef.isAoe && AOE {golemDef.aoeTargets}}
-
- ⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr • - 🗡️ {Math.floor(golemDef.armorPierce * 100)}% Pierce -
- {/* Attack progress bar when climbing */} +
⚔️ {damage} DMG • ⚡ {attackSpeed.toFixed(1)}/hr
{store.currentAction === 'climb' && summoned.attackProgress > 0 && ( -
-
+
+
Attack - {Math.min(100, (summoned.attackProgress * 100)).toFixed(0)}% + {(summoned.attackProgress * 100).toFixed(0)}% +
+
+
-
)}
); })} - - - )} - - {/* Current Study (if any) - Only show in normal mode */} - {!simpleMode && store.currentStudyTarget && ( - - - - - {/* Parallel Study Progress */} - {store.parallelStudyTarget && ( -
-
-
- - - Parallel: {SKILLS_DEF[store.parallelStudyTarget.id]?.name} - {store.parallelStudyTarget.type === 'skill' && ` Lv.${(store.skills[store.parallelStudyTarget.id] || 0) + 1}`} - -
- -
- -
- {formatStudyTime(store.parallelStudyTarget.progress)} / {formatStudyTime(store.parallelStudyTarget.required)} - 50% speed (Parallel Study) -
+
+
+
+ )} + + {/* Guardian Panel */} + {isGuardianFloor && simpleMode && ( + + )} + + {/* Room Display */} + {simpleMode && ( + + )} + + {/* Floor Controls */} + {simpleMode && ( + + )} + + {/* Combat Stats Panel */} + {simpleMode && ( + + )} + + {/* Activity Log - Spire Mode only */} + {simpleMode && } + + {/* Study Progress - Normal mode only */} + {!simpleMode && store.currentStudyTarget && ( + + +
Study: {getSkillName(store.currentStudyTarget.id)}
+
+
+
+ {store.parallelStudyTarget && ( +
+
Parallel: {getSkillName(store.parallelStudyTarget.id)} (50% speed)
+
+
- )} - - - )} - - {/* Activity Log - Show in Spire Mode (simpleMode) */} - {simpleMode && ( - - - Activity Log - - - -
- {(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 ( -
- {entry.message} -
- ); - })} - {(store.activityLog || []).length === 0 && ( -
No activity yet...
- )} +
+ )} +
+
+ )} + + {/* Crafting Progress - Normal mode only */} + {!simpleMode && (store.designProgress || store.preparationProgress || store.applicationProgress) && ( + + + {store.designProgress && ( +
+
Design Progress
+
+
- - - - )} - - {/* Crafting Progress (if any) - Only show in normal mode */} - {!simpleMode && (store.designProgress || store.preparationProgress || store.applicationProgress) && ( - - - - - - )} -
- +
+ )} + {store.preparationProgress && ( +
+
Preparation Progress
+
+
+
+
+ )} + {store.applicationProgress && ( +
+
Application Progress
+
+
+
+
+ )} + + + )} +
); } 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); +} diff --git a/src/components/game/tabs/index.ts b/src/components/game/tabs/index.ts index 92431b4..312d95f 100755 --- a/src/components/game/tabs/index.ts +++ b/src/components/game/tabs/index.ts @@ -13,3 +13,11 @@ export { DebugTab } from './DebugTab'; export { LootTab } from './LootTab'; export { AchievementsTab } from './AchievementsTab'; 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'; diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts index 5993508..49a6287 100755 --- a/src/lib/game/types.ts +++ b/src/lib/game/types.ts @@ -1,14 +1,64 @@ -// ─── Game Types ─────────────────────────────────────────────────────────────── -// This file now re-exports types from domain-specific files in the types/ directory. -// All type definitions have been moved to: -// - types/elements.ts - element-related types -// - 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. +import type { GameStore } from '@/lib/game/store'; +import type { SPELLS } from '@/lib/game/constants'; +import type { EquipmentSpellState } from '@/lib/game/state'; +import type { RoomType, ActivityLogEntry } from '@/lib/game/types/game'; -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; +}