diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 3a859c3..ba2b365 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,8 +1,8 @@ # Circular Dependencies -Generated: 2026-05-19T20:26:04.052Z +Generated: 2026-05-19T20:37:58.097Z Found: 3 circular chain(s) — these MUST be fixed before modifying involved files. -1. Processed 121 files (1.3s) (4 warnings) +1. Processed 121 files (1.2s) (4 warnings) 2. 1) data/equipment/index.ts > data/equipment/utils.ts 3. 2) data/golems/index.ts > data/golems/utils.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index e79dbc5..dc26a7f 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-19T20:26:02.602Z", + "generated": "2026-05-19T20:37:56.730Z", "description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.", "usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry." }, diff --git a/docs/project-structure.txt b/docs/project-structure.txt index 43119e6..08bfea2 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -124,6 +124,8 @@ Mana-Loop/ │ │ │ │ ├── PrestigeTab.test.ts │ │ │ │ ├── PrestigeTab.tsx │ │ │ │ ├── SpellsTab.tsx +│ │ │ │ ├── SpireSummaryTab.test.ts +│ │ │ │ ├── SpireSummaryTab.tsx │ │ │ │ ├── StatsTab.tsx │ │ │ │ └── index.ts │ │ │ ├── ActionButtons.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 4f017e2..e5418f1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -51,6 +51,7 @@ const PrestigeTab = lazy(() => import('@/components/game/tabs').then(module => ( const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab }))); const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab }))); const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GuardianPactsTab }))); +const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireSummaryTab }))); const TabLoadingFallback = () =>
Loading...
; @@ -247,6 +248,7 @@ export default function ManaLoopGame() { ⚔️ Equipment 🗿 Golemancy 📜 Pacts + 🏔️ Spire @@ -332,6 +334,14 @@ export default function ManaLoopGame() { + + + spire tab failed to load.}> + }> + + + + diff --git a/src/components/game/tabs/SpireSummaryTab.test.ts b/src/components/game/tabs/SpireSummaryTab.test.ts new file mode 100644 index 0000000..dcf1453 --- /dev/null +++ b/src/components/game/tabs/SpireSummaryTab.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from 'vitest'; + +// ─── Test: SpireSummaryTab barrel export ─────────────────────────────────────── + +describe('SpireSummaryTab module structure', () => { + it('exports SpireSummaryTab from its module', async () => { + const mod = await import('./SpireSummaryTab'); + expect(mod.SpireSummaryTab).toBeDefined(); + expect(typeof mod.SpireSummaryTab).toBe('function'); + }); + + it('SpireSummaryTab has correct displayName', async () => { + const { SpireSummaryTab } = await import('./SpireSummaryTab'); + expect(SpireSummaryTab.displayName).toBe('SpireSummaryTab'); + }); +}); + +// ─── Test: Barrel export includes SpireSummaryTab ────────────────────────────── + +describe('Tab barrel export', () => { + it('includes SpireSummaryTab in the tabs index', async () => { + const mod = await import('@/components/game/tabs'); + expect(mod.SpireSummaryTab).toBeDefined(); + expect(typeof mod.SpireSummaryTab).toBe('function'); + }); +}); + +// ─── Test: Guardian data ─────────────────────────────────────────────────────── + +describe('Guardian constants', () => { + it('has 9 guardians defined', async () => { + const { GUARDIANS } = await import('@/lib/game/constants'); + expect(Object.keys(GUARDIANS).length).toBe(9); + }); + + it('guardians are at expected floors', async () => { + const { GUARDIANS } = await import('@/lib/game/constants'); + const expectedFloors = [10, 20, 30, 40, 50, 60, 80, 90, 100]; + for (const floor of expectedFloors) { + expect(GUARDIANS[floor]).toBeDefined(); + } + }); + + it('all guardians have required fields', async () => { + const { GUARDIANS } = await import('@/lib/game/constants'); + for (const [, def] of Object.entries(GUARDIANS)) { + expect(def.name).toBeTruthy(); + expect(def.element).toBeTruthy(); + expect(def.hp).toBeGreaterThan(0); + expect(def.color).toBeTruthy(); + expect(def.boons).toBeInstanceOf(Array); + expect(def.boons.length).toBeGreaterThan(0); + } + }); + + it('all guardians have unique elements', async () => { + const { GUARDIANS } = await import('@/lib/game/constants'); + const elements = Object.values(GUARDIANS).map((g) => g.element); + const uniqueElements = new Set(elements); + expect(uniqueElements.size).toBe(elements.length); + }); +}); + +// ─── Test: Combat store spire fields ─────────────────────────────────────────── + +describe('Combat store spire state', () => { + it('useCombatStore is importable', async () => { + const mod = await import('@/lib/game/stores'); + expect(mod.useCombatStore).toBeDefined(); + expect(typeof mod.useCombatStore).toBe('function'); + }); +}); + +// ─── Test: Floor element cycle ───────────────────────────────────────────────── + +describe('Floor element cycle', () => { + it('FLOOR_ELEM_CYCLE has 7 elements', async () => { + const { FLOOR_ELEM_CYCLE } = await import('@/lib/game/constants'); + expect(FLOOR_ELEM_CYCLE.length).toBe(7); + }); + + it('element opposites define expected pairs', async () => { + const { ELEMENT_OPPOSITES } = await import('@/lib/game/constants'); + // Core pairs are symmetric + expect(ELEMENT_OPPOSITES['fire']).toBe('water'); + expect(ELEMENT_OPPOSITES['water']).toBe('fire'); + expect(ELEMENT_OPPOSITES['air']).toBe('earth'); + expect(ELEMENT_OPPOSITES['earth']).toBe('air'); + expect(ELEMENT_OPPOSITES['light']).toBe('dark'); + expect(ELEMENT_OPPOSITES['dark']).toBe('light'); + // Lightning has an asymmetric opposite (grounding) + expect(ELEMENT_OPPOSITES['lightning']).toBe('earth'); + }); +}); + +// ─── Test: File size limit ───────────────────────────────────────────────────── + +describe('File size limits (400 lines max)', () => { + it('SpireSummaryTab.tsx is under 400 lines', async () => { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join(__dirname, 'SpireSummaryTab.tsx'); + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n').length; + expect(lines).toBeLessThan(400); + }); +}); diff --git a/src/components/game/tabs/SpireSummaryTab.tsx b/src/components/game/tabs/SpireSummaryTab.tsx new file mode 100644 index 0000000..4ef9708 --- /dev/null +++ b/src/components/game/tabs/SpireSummaryTab.tsx @@ -0,0 +1,356 @@ +'use client'; + +import { useState, useEffect, useMemo } from 'react'; +import { useShallow } from 'zustand/react/shallow'; +import { useCombatStore, usePrestigeStore, fmt } from '@/lib/game/stores'; +import { GUARDIANS, ELEMENT_OPPOSITES, FLOOR_ELEM_CYCLE } from '@/lib/game/constants'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { SectionHeader } from '@/components/ui/section-header'; +import { DebugName } from '@/components/game/debug/debug-context'; +import { Mountain } from 'lucide-react'; + +// ─── Guardian Data ──────────────────────────────────────────────────────────── + +const GUARDIAN_FLOORS = Object.keys(GUARDIANS) + .map(Number) + .sort((a, b) => a - b); + +// ─── Helper: Get Counter Element ───────────────────────────────────────────── + +function getCounterElement(element: string): string | null { + return ELEMENT_OPPOSITES[element] || null; +} + +function getElementColor(element: string): string { + const colors: Record = { + fire: '#FF6B35', + water: '#4ECDC4', + air: '#00D4FF', + earth: '#F4A261', + light: '#FFD700', + dark: '#9B59B6', + death: '#778CA3', + void: '#4A235A', + stellar: '#F0E68C', + }; + return colors[element] || '#9CA3AF'; +} + +// ─── Sub-component: Floor Progress Bar ──────────────────────────────────────── + +function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clearedFloors: Record }) { + const totalFloors = Math.min(maxFloor, 100); + const clearedSet = new Set(Object.entries(clearedFloors).filter(([, v]) => v).map(([k]) => Number(k))); + + // Group floors into rows of 10 for display + const rows: number[][] = []; + for (let i = 0; i < totalFloors; i += 10) { + rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors)); + } + + return ( +
+ {rows.reverse().map((row) => ( +
+ {row.map((floor) => { + const isCleared = clearedSet.has(floor); + const isGuardian = !!GUARDIANS[floor]; + const isCurrent = floor === maxFloor; + + let bgClass = 'bg-gray-800'; + if (isCleared) bgClass = 'bg-emerald-600/60'; + else if (isCurrent) bgClass = 'bg-amber-600/60'; + + const borderClass = isGuardian + ? 'border-amber-500' + : isCurrent + ? 'border-amber-400' + : 'border-gray-700'; + + return ( +
+ {floor} +
+ ); + })} +
+ ))} +
+
+
+ Cleared +
+
+
+ Uncleared +
+
+
+ Guardian +
+
+
+ Current +
+
+
+ ); +} + +// ─── Main Component ─────────────────────────────────────────────────────────── + +export function SpireSummaryTab() { + const [mounted, setMounted] = useState(false); + + const { + currentFloor, + maxFloorReached, + clearedFloors, + enterSpireMode, + } = useCombatStore(useShallow((s) => ({ + currentFloor: s.currentFloor, + maxFloorReached: s.maxFloorReached, + clearedFloors: s.clearedFloors, + enterSpireMode: s.enterSpireMode, + }))); + + const { insight } = usePrestigeStore(useShallow((s) => ({ + insight: s.insight, + }))); + + useEffect(() => { + setMounted(true); + }, []); + + // Derived data + const defeatedGuardians = useMemo(() => { + return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]); + }, [clearedFloors]); + + const nextGuardian = useMemo(() => { + return GUARDIAN_FLOORS.find((floor) => !clearedFloors[floor]) || null; + }, [clearedFloors]); + + const nextGuardianData = nextGuardian ? GUARDIANS[nextGuardian] : null; + + const counterElement = nextGuardianData ? getCounterElement(nextGuardianData.element) : null; + const nextFloorElement = nextGuardian ? FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length] : null; + + const totalFloorsCleared = useMemo(() => { + return Object.values(clearedFloors).filter(Boolean).length; + }, [clearedFloors]); + + if (!mounted) { + return ( +
+ Loading spire data… +
+ ); + } + + return ( + +
+ {/* ── Top Stats Row ─────────────────────────────────────────────── */} + + +
+
+
{maxFloorReached}
+
Max Floor Reached
+
+
+
{totalFloorsCleared}
+
Floors Cleared
+
+
+
{defeatedGuardians.length}
+
Guardians Defeated
+
+
+
{fmt(insight)}
+
Insight Earned
+
+
+
+
+ + {/* ── Climb the Spire Button ────────────────────────────────────── */} + + + + + {/* ── Next Guardian + Preparation ───────────────────────────────── */} + {nextGuardianData && nextGuardian && ( + + + +
+
+ {nextGuardian} +
+
+
{nextGuardianData.name}
+
+ + {nextGuardianData.element} + + HP: {fmt(nextGuardianData.hp)} + {nextGuardianData.armor && ( + + Armor: {Math.round(nextGuardianData.armor * 100)}% + + )} +
+
+
+ + {/* Preparation recommendations */} +
+
Recommended Preparation:
+
+ {counterElement && ( +
+ + + Use {counterElement} spells for super effective damage (+50%) + +
+ )} + {nextFloorElement && ( +
+ 🔄 + + Floor element: {nextFloorElement} + +
+ )} + {nextGuardianData.armor && nextGuardianData.armor > 0.15 && ( +
+ 🛡️ + High armor — consider armor-piercing or raw damage spells +
+ )} +
+ 💡 + Ensure mana pools are full before attempting +
+
+
+
+
+ )} + + {/* ── All Guardians List ────────────────────────────────────────── */} + + + +
+ {GUARDIAN_FLOORS.map((floor) => { + const guardian = GUARDIANS[floor]; + const isDefeated = clearedFloors[floor]; + + return ( +
+
+
+ {floor} +
+
+
+ {guardian.name} +
+
+ + {guardian.element} + + HP: {fmt(guardian.hp)} +
+
+
+
+ {isDefeated ? ( + + ✓ Defeated + + ) : ( + + Undefeated + + )} +
+
+ ); + })} +
+
+
+ + {/* ── Floor Progress Map ────────────────────────────────────────── */} + + + + + + + + +
+
+ ); +} + +SpireSummaryTab.displayName = 'SpireSummaryTab'; diff --git a/src/components/game/tabs/index.ts b/src/components/game/tabs/index.ts index e870f5b..7d745ba 100644 --- a/src/components/game/tabs/index.ts +++ b/src/components/game/tabs/index.ts @@ -11,3 +11,4 @@ export { PrestigeTab } from './PrestigeTab'; export { EquipmentTab } from './EquipmentTab'; export { GolemancyTab } from './GolemancyTab'; export { GuardianPactsTab } from './GuardianPactsTab'; +export { SpireSummaryTab } from './SpireSummaryTab';