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 */}
+
+
+
+
+ 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}