diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index cc69d6a..b21437a 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-05-19T11:53:37.574Z +Generated: 2026-05-19T12:44:32.003Z Found: 3 circular chain(s) — these MUST be fixed before modifying involved files. 1. Processed 121 files (1.2s) (4 warnings) diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index 7f0dec5..9507e54 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-05-19T11:53:36.198Z", + "generated": "2026-05-19T12:44:30.633Z", "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 1d3a0c5..4297d75 100644 --- a/docs/project-structure.txt +++ b/docs/project-structure.txt @@ -106,6 +106,8 @@ Mana-Loop/ │ │ │ │ │ └── StudyStatsSection.tsx │ │ │ │ ├── AchievementsTab.tsx │ │ │ │ ├── ActivityLog.tsx +│ │ │ │ ├── DebugTab.test.ts +│ │ │ │ ├── DebugTab.tsx │ │ │ │ ├── DisciplinesTab.tsx │ │ │ │ ├── SpellsTab.tsx │ │ │ │ ├── StatsTab.tsx diff --git a/src/app/page.tsx b/src/app/page.tsx index 44809a5..0837bf3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -44,6 +44,7 @@ import { LeftPanel } from './components/LeftPanel'; const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DisciplinesTab }))); const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab }))); const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab }))); +const DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab }))); const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab }))); const TabLoadingFallback = () =>
Loading...
; @@ -234,6 +235,7 @@ export default function ManaLoopGame() { 📊 Stats 📚 Disciplines 📖 Grimoire + 🐛 Debug 🏆 Achievements @@ -265,6 +267,14 @@ export default function ManaLoopGame() { + + debug tab failed to load.}> + }> + + + + + achievements tab failed to load.}> }> diff --git a/src/components/game/tabs/DebugTab.test.ts b/src/components/game/tabs/DebugTab.test.ts new file mode 100644 index 0000000..a15416c --- /dev/null +++ b/src/components/game/tabs/DebugTab.test.ts @@ -0,0 +1,337 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// ─── Test: DebugTab barrel export ───────────────────────────────────────────── +// Verifies that the DebugTab component is properly exported from the barrel +// and that all section components are importable. + +describe('DebugTab module structure', () => { + it('exports DebugTab from barrel index', async () => { + const mod = await import('./DebugTab'); + expect(mod.DebugTab).toBeDefined(); + expect(typeof mod.DebugTab).toBe('function'); + }); + + it('exports GameStateDebugSection', async () => { + const mod = await import('./DebugTab/GameStateDebugSection'); + expect(mod.GameStateDebugSection).toBeDefined(); + expect(typeof mod.GameStateDebugSection).toBe('function'); + }); + + it('exports DisciplineDebugSection', async () => { + const mod = await import('./DebugTab/DisciplineDebugSection'); + expect(mod.DisciplineDebugSection).toBeDefined(); + expect(typeof mod.DisciplineDebugSection).toBe('function'); + }); + + it('exports AttunementDebugSection', async () => { + const mod = await import('./DebugTab/AttunementDebugSection'); + expect(mod.AttunementDebugSection).toBeDefined(); + expect(typeof mod.AttunementDebugSection).toBe('function'); + }); + + it('exports ElementDebugSection', async () => { + const mod = await import('./DebugTab/ElementDebugSection'); + expect(mod.ElementDebugSection).toBeDefined(); + expect(typeof mod.ElementDebugSection).toBe('function'); + }); + + it('exports GolemDebugSection', async () => { + const mod = await import('./DebugTab/GolemDebugSection'); + expect(mod.GolemDebugSection).toBeDefined(); + expect(typeof mod.GolemDebugSection).toBe('function'); + }); + + it('exports PactDebugSection', async () => { + const mod = await import('./DebugTab/PactDebugSection'); + expect(mod.PactDebugSection).toBeDefined(); + expect(typeof mod.PactDebugSection).toBe('function'); + }); + + it('exports SpireDebugSection', async () => { + const mod = await import('./DebugTab/SpireDebugSection'); + expect(mod.SpireDebugSection).toBeDefined(); + expect(typeof mod.SpireDebugSection).toBe('function'); + }); + + it('exports AchievementDebugSection', async () => { + const mod = await import('./DebugTab/AchievementDebugSection'); + expect(mod.AchievementDebugSection).toBeDefined(); + expect(typeof mod.AchievementDebugSection).toBe('function'); + }); +}); + +// ─── Test: Barrel export includes DebugTab ──────────────────────────────────── + +describe('Tab barrel export', () => { + it('includes DebugTab in the tabs index', async () => { + const mod = await import('@/components/game/tabs'); + expect(mod.DebugTab).toBeDefined(); + expect(typeof mod.DebugTab).toBe('function'); + }); +}); + +// ─── Test: Store interactions used by DebugTab sections ─────────────────────── + +describe('GameStateDebugSection store interactions', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('resetGame action is callable', () => { + const mockReset = vi.fn(); + // Simulate what GameStateDebugSection does on reset + mockReset(); + expect(mockReset).toHaveBeenCalledTimes(1); + }); + + it('gatherMana action is callable N times for bulk add', () => { + const mockGather = vi.fn(); + const amount = 100; + for (let i = 0; i < amount; i++) { + mockGather(); + } + expect(mockGather).toHaveBeenCalledTimes(amount); + }); + + it('togglePause action is callable', () => { + const mockToggle = vi.fn(); + mockToggle(); + expect(mockToggle).toHaveBeenCalledTimes(1); + }); + + it('debugSetFloor action is callable with floor number', () => { + const mockSetFloor = vi.fn(); + mockSetFloor(100); + expect(mockSetFloor).toHaveBeenCalledWith(100); + }); + + it('resetFloorHP action is callable', () => { + const mockResetHP = vi.fn(); + mockResetHP(); + expect(mockResetHP).toHaveBeenCalledTimes(1); + }); +}); + +describe('DisciplineDebugSection store interactions', () => { + it('activate action is callable', () => { + const mockActivate = vi.fn(); + mockActivate('meditation'); + expect(mockActivate).toHaveBeenCalledWith('meditation'); + }); + + it('deactivate action is callable', () => { + const mockDeactivate = vi.fn(); + mockDeactivate('meditation'); + expect(mockDeactivate).toHaveBeenCalledWith('meditation'); + }); + + it('XP can be added to discipline via setState', () => { + const disciplines: Record = { + meditation: { xp: 0, paused: false }, + }; + const id = 'meditation'; + const amount = 100; + disciplines[id] = { ...disciplines[id], xp: disciplines[id].xp + amount }; + expect(disciplines[id].xp).toBe(100); + }); +}); + +describe('AttunementDebugSection store interactions', () => { + it('debugUnlockAttunement is callable', () => { + const mockUnlock = vi.fn(); + mockUnlock('invoker'); + expect(mockUnlock).toHaveBeenCalledWith('invoker'); + }); + + it('addAttunementXP is callable', () => { + const mockAddXP = vi.fn(); + mockAddXP('enchanter', 100); + expect(mockAddXP).toHaveBeenCalledWith('enchanter', 100); + }); +}); + +describe('ElementDebugSection store interactions', () => { + it('unlockElement is callable with zero cost', () => { + const mockUnlock = vi.fn(); + mockUnlock('fire', 0); + expect(mockUnlock).toHaveBeenCalledWith('fire', 0); + }); + + it('addElementMana is callable', () => { + const mockAdd = vi.fn(); + mockAdd('fire', 10, 50); + expect(mockAdd).toHaveBeenCalledWith('fire', 10, 50); + }); +}); + +describe('GolemDebugSection store interactions', () => { + it('setEnabledGolems is callable with all golem IDs', () => { + const mockSet = vi.fn(); + const allIds = ['stoneGolem', 'fireGolem']; + mockSet(allIds); + expect(mockSet).toHaveBeenCalledWith(allIds); + }); + + it('setEnabledGolems is callable with empty array to disable all', () => { + const mockSet = vi.fn(); + mockSet([]); + expect(mockSet).toHaveBeenCalledWith([]); + }); +}); + +describe('PactDebugSection store interactions', () => { + it('addSignedPact is callable', () => { + const mockAdd = vi.fn(); + mockAdd(10); + expect(mockAdd).toHaveBeenCalledWith(10); + }); + + it('removePact is callable', () => { + const mockRemove = vi.fn(); + mockRemove(10); + expect(mockRemove).toHaveBeenCalledWith(10); + }); + + it('debugSetSignedPacts is callable', () => { + const mockSet = vi.fn(); + mockSet([10, 20, 30]); + expect(mockSet).toHaveBeenCalledWith([10, 20, 30]); + }); +}); + +describe('SpireDebugSection store interactions', () => { + it('enterSpireMode is callable', () => { + const mockEnter = vi.fn(); + mockEnter(); + expect(mockEnter).toHaveBeenCalledTimes(1); + }); + + it('exitSpireMode is callable', () => { + const mockExit = vi.fn(); + mockExit(); + expect(mockExit).toHaveBeenCalledTimes(1); + }); + + it('setMaxFloorReached is callable', () => { + const mockSet = vi.fn(); + mockSet(50); + expect(mockSet).toHaveBeenCalledWith(50); + }); +}); + +describe('AchievementDebugSection store interactions', () => { + it('can set all achievements as unlocked via setState', () => { + const allIds = ['firstBlood', 'floorClimber']; + const newState = { + achievements: { + unlocked: allIds, + progress: Object.fromEntries(allIds.map(id => [id, 100])), + }, + }; + expect(newState.achievements.unlocked).toEqual(allIds); + expect(Object.keys(newState.achievements.progress)).toEqual(allIds); + }); + + it('can reset all achievements via setState', () => { + const newState = { + achievements: { + unlocked: [], + progress: {}, + }, + }; + expect(newState.achievements.unlocked).toEqual([]); + expect(newState.achievements.progress).toEqual({}); + }); +}); + +// ─── Test: DebugTab component displayName ───────────────────────────────────── + +describe('DebugTab component metadata', () => { + it('DebugTab has correct displayName', async () => { + const { DebugTab } = await import('./DebugTab'); + expect(DebugTab.displayName).toBe('DebugTab'); + }); + + it('GameStateDebugSection has correct displayName', async () => { + const { GameStateDebugSection } = await import('./DebugTab/GameStateDebugSection'); + expect(GameStateDebugSection.displayName).toBe('GameStateDebugSection'); + }); + + it('DisciplineDebugSection has correct displayName', async () => { + const { DisciplineDebugSection } = await import('./DebugTab/DisciplineDebugSection'); + expect(DisciplineDebugSection.displayName).toBe('DisciplineDebugSection'); + }); + + it('AttunementDebugSection has correct displayName', async () => { + const { AttunementDebugSection } = await import('./DebugTab/AttunementDebugSection'); + expect(AttunementDebugSection.displayName).toBe('AttunementDebugSection'); + }); + + it('ElementDebugSection has correct displayName', async () => { + const { ElementDebugSection } = await import('./DebugTab/ElementDebugSection'); + expect(ElementDebugSection.displayName).toBe('ElementDebugSection'); + }); + + it('GolemDebugSection has correct displayName', async () => { + const { GolemDebugSection } = await import('./DebugTab/GolemDebugSection'); + expect(GolemDebugSection.displayName).toBe('GolemDebugSection'); + }); + + it('PactDebugSection has correct displayName', async () => { + const { PactDebugSection } = await import('./DebugTab/PactDebugSection'); + expect(PactDebugSection.displayName).toBe('PactDebugSection'); + }); + + it('SpireDebugSection has correct displayName', async () => { + const { SpireDebugSection } = await import('./DebugTab/SpireDebugSection'); + expect(SpireDebugSection.displayName).toBe('SpireDebugSection'); + }); + + it('AchievementDebugSection has correct displayName', async () => { + const { AchievementDebugSection } = await import('./DebugTab/AchievementDebugSection'); + expect(AchievementDebugSection.displayName).toBe('AchievementDebugSection'); + }); +}); + +// ─── Test: File size limits ─────────────────────────────────────────────────── +// Note: 400-line limit is enforced by pre-commit hook (check-file-size.js). +// These tests verify the source files are importable; line count enforcement +// is handled by the hook, not by runtime tests. + +describe('File size limits (400 lines max)', () => { + it('DebugTab.tsx is importable and under 400 lines (enforced by pre-commit hook)', async () => { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join(__dirname, 'DebugTab.tsx'); + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n').length; + expect(lines).toBeLessThan(400); + }); + + it('GameStateDebugSection.tsx is under 400 lines', async () => { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join(__dirname, 'DebugTab', 'GameStateDebugSection.tsx'); + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n').length; + expect(lines).toBeLessThan(400); + }); + + it('DisciplineDebugSection.tsx is under 400 lines', async () => { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join(__dirname, 'DebugTab', 'DisciplineDebugSection.tsx'); + const content = fs.readFileSync(filePath, 'utf-8'); + const lines = content.split('\n').length; + expect(lines).toBeLessThan(400); + }); + + it('PactDebugSection.tsx is under 400 lines', async () => { + const fs = await import('fs'); + const path = await import('path'); + const filePath = path.join(__dirname, 'DebugTab', 'PactDebugSection.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/DebugTab.tsx b/src/components/game/tabs/DebugTab.tsx new file mode 100644 index 0000000..e687911 --- /dev/null +++ b/src/components/game/tabs/DebugTab.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { useState } from 'react'; +import { Card, CardContent } from '@/components/ui/card'; +import { AlertTriangle, ChevronDown, ChevronRight } from 'lucide-react'; +import { DebugName } from '@/components/game/debug/debug-context'; +import { GameStateDebugSection } from './DebugTab/GameStateDebugSection'; +import { DisciplineDebugSection } from './DebugTab/DisciplineDebugSection'; +import { AttunementDebugSection } from './DebugTab/AttunementDebugSection'; +import { ElementDebugSection } from './DebugTab/ElementDebugSection'; +import { GolemDebugSection } from './DebugTab/GolemDebugSection'; +import { PactDebugSection } from './DebugTab/PactDebugSection'; +import { SpireDebugSection } from './DebugTab/SpireDebugSection'; +import { AchievementDebugSection } from './DebugTab/AchievementDebugSection'; + +interface DebugSectionProps { + title: string; + color: string; + children: React.ReactNode; + defaultOpen?: boolean; +} + +function DebugSection({ title, color, children, defaultOpen = false }: DebugSectionProps) { + const [isOpen, setIsOpen] = useState(defaultOpen); + + return ( + + + {isOpen && ( + + {children} + + )} + + ); +} + +export function DebugTab() { + return ( + +
+ {/* Warning Banner */} + + +
+ + Debug Mode +
+

+ These tools are for development and testing. Using them may break game balance or save data. +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ ); +} + +DebugTab.displayName = "DebugTab"; diff --git a/src/components/game/tabs/DebugTab/GolemDebugSection.tsx b/src/components/game/tabs/DebugTab/GolemDebugSection.tsx index 0d5fe27..5250af0 100644 --- a/src/components/game/tabs/DebugTab/GolemDebugSection.tsx +++ b/src/components/game/tabs/DebugTab/GolemDebugSection.tsx @@ -60,7 +60,7 @@ export function GolemDebugSection() { >
{def.name}
-
{def.element}
+
{def.baseManaType}