diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt
index 4bba053..2fd2b6a 100644
--- a/docs/circular-deps.txt
+++ b/docs/circular-deps.txt
@@ -1,5 +1,5 @@
# Circular Dependencies
-Generated: 2026-05-19T13:55:24.489Z
+Generated: 2026-05-19T16:29:33.501Z
Found: 3 circular chain(s) — these MUST be fixed before modifying involved files.
1. Processed 121 files (1.3s) (4 warnings)
diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json
index e463c78..5422abd 100644
--- a/docs/dependency-graph.json
+++ b/docs/dependency-graph.json
@@ -1,6 +1,6 @@
{
"_meta": {
- "generated": "2026-05-19T13:55:23.066Z",
+ "generated": "2026-05-19T16:29:32.045Z",
"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 5c2a7d6..b2b8f02 100644
--- a/docs/project-structure.txt
+++ b/docs/project-structure.txt
@@ -111,6 +111,8 @@ Mana-Loop/
│ │ │ │ ├── DebugTab.test.ts
│ │ │ │ ├── DebugTab.tsx
│ │ │ │ ├── DisciplinesTab.tsx
+│ │ │ │ ├── PrestigeTab.test.ts
+│ │ │ │ ├── PrestigeTab.tsx
│ │ │ │ ├── SpellsTab.tsx
│ │ │ │ ├── StatsTab.tsx
│ │ │ │ └── index.ts
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 737ce99..4620805 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -47,6 +47,7 @@ const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ d
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 AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab })));
+const PrestigeTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.PrestigeTab })));
const TabLoadingFallback = () =>
Loading...
;
@@ -239,6 +240,7 @@ export default function ManaLoopGame() {
🐛 Debug
⚗️ Attunements
🏆 Achievements
+ ✨ Prestige
@@ -292,6 +294,14 @@ export default function ManaLoopGame() {
+
+
+ prestige tab failed to load.}>
+ }>
+
+
+
+
diff --git a/src/components/game/tabs/PrestigeTab.test.ts b/src/components/game/tabs/PrestigeTab.test.ts
new file mode 100644
index 0000000..88bc23a
--- /dev/null
+++ b/src/components/game/tabs/PrestigeTab.test.ts
@@ -0,0 +1,97 @@
+import { describe, it, expect } from 'vitest';
+
+// ─── Test: PrestigeTab barrel export ───────────────────────────────────────────
+
+describe('PrestigeTab module structure', () => {
+ it('exports PrestigeTab from barrel index', async () => {
+ const mod = await import('./PrestigeTab');
+ expect(mod.PrestigeTab).toBeDefined();
+ expect(typeof mod.PrestigeTab).toBe('function');
+ });
+
+ it('PrestigeTab has correct displayName', async () => {
+ const { PrestigeTab } = await import('./PrestigeTab');
+ expect(PrestigeTab.displayName).toBe('PrestigeTab');
+ });
+});
+
+// ─── Test: Barrel export includes PrestigeTab ──────────────────────────────────
+
+describe('Tab barrel export', () => {
+ it('includes PrestigeTab in the tabs index', async () => {
+ const mod = await import('@/components/game/tabs');
+ expect(mod.PrestigeTab).toBeDefined();
+ expect(typeof mod.PrestigeTab).toBe('function');
+ });
+});
+
+// ─── Test: Prestige upgrade definitions ────────────────────────────────────────
+
+describe('Prestige upgrade definitions', () => {
+ it('has exactly 14 prestige upgrades', async () => {
+ const { PRESTIGE_DEF } = await import('@/lib/game/constants/prestige');
+ expect(Object.keys(PRESTIGE_DEF).length).toBe(14);
+ });
+
+ it('all upgrades have required fields', async () => {
+ const { PRESTIGE_DEF } = await import('@/lib/game/constants/prestige');
+ for (const [id, def] of Object.entries(PRESTIGE_DEF)) {
+ expect(def.name).toBeTruthy();
+ expect(def.desc).toBeTruthy();
+ expect(def.max).toBeGreaterThan(0);
+ expect(def.cost).toBeGreaterThan(0);
+ }
+ });
+
+ it('all 14 expected upgrade IDs are present', async () => {
+ const { PRESTIGE_DEF } = await import('@/lib/game/constants/prestige');
+ const expectedIds = [
+ 'manaWell', 'manaFlow', 'deepMemory', 'insightAmp', 'spireKey',
+ 'temporalEcho', 'steadyHand', 'ancientKnowledge', 'elementalAttune',
+ 'spellMemory', 'guardianPact', 'quickStart', 'elemStart',
+ 'unlockedManaTypeCapacity',
+ ];
+ for (const id of expectedIds) {
+ expect(PRESTIGE_DEF[id]).toBeDefined();
+ }
+ });
+
+ it('upgrade costs are positive integers', async () => {
+ const { PRESTIGE_DEF } = await import('@/lib/game/constants/prestige');
+ for (const def of Object.values(PRESTIGE_DEF)) {
+ expect(Number.isInteger(def.cost)).toBe(true);
+ expect(def.cost).toBeGreaterThan(0);
+ }
+ });
+
+ it('upgrade max levels are positive integers', async () => {
+ const { PRESTIGE_DEF } = await import('@/lib/game/constants/prestige');
+ for (const def of Object.values(PRESTIGE_DEF)) {
+ expect(Number.isInteger(def.max)).toBe(true);
+ expect(def.max).toBeGreaterThan(0);
+ }
+ });
+});
+
+// ─── Test: Prestige store shape ────────────────────────────────────────────────
+
+describe('Prestige store', () => {
+ it('usePrestigeStore is importable', async () => {
+ const mod = await import('@/lib/game/stores');
+ expect(mod.usePrestigeStore).toBeDefined();
+ expect(typeof mod.usePrestigeStore).toBe('function');
+ });
+});
+
+// ─── Test: File size limit ─────────────────────────────────────────────────────
+
+describe('File size limits (400 lines max)', () => {
+ it('PrestigeTab.tsx is under 400 lines', async () => {
+ const fs = await import('fs');
+ const path = await import('path');
+ const filePath = path.join(__dirname, 'PrestigeTab.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/PrestigeTab.tsx b/src/components/game/tabs/PrestigeTab.tsx
new file mode 100644
index 0000000..5387b30
--- /dev/null
+++ b/src/components/game/tabs/PrestigeTab.tsx
@@ -0,0 +1,272 @@
+'use client';
+
+import { useState, useEffect, useCallback } from 'react';
+import { useShallow } from 'zustand/react/shallow';
+import { usePrestigeStore, useGameStore } from '@/lib/game/stores';
+import { PRESTIGE_DEF } from '@/lib/game/constants/prestige';
+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 { fmt } from '@/lib/game/stores';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/alert-dialog';
+
+// ─── Upgrade Card ─────────────────────────────────────────────────────────────
+
+interface UpgradeCardProps {
+ id: string;
+ name: string;
+ desc: string;
+ max: number;
+ cost: number;
+ currentLevel: number;
+ insight: number;
+ onPurchase: (id: string) => void;
+}
+
+function UpgradeCard({ id, name, desc, max, cost, currentLevel, insight, onPurchase }: UpgradeCardProps) {
+ const isMaxed = currentLevel >= max;
+ const canAfford = insight >= cost;
+ const disabled = isMaxed || !canAfford;
+
+ return (
+
+
+
+ {name}
+
+ {currentLevel}/{max}
+
+
+ {desc}
+
+
+ Cost: {fmt(cost)} insight
+
+ onPurchase(id)}
+ className="h-7 px-3 text-xs"
+ >
+ {isMaxed ? 'Maxed' : 'Buy'}
+
+
+
+
+ );
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
+export function PrestigeTab() {
+ const [mounted, setMounted] = useState(false);
+
+ const {
+ insight,
+ totalInsight,
+ loopInsight,
+ loopCount,
+ prestigeUpgrades,
+ memorySlots,
+ memories,
+ pactSlots,
+ signedPacts,
+ defeatedGuardians,
+ doPrestige,
+ } = usePrestigeStore(useShallow((s) => ({
+ insight: s.insight,
+ totalInsight: s.totalInsight,
+ loopInsight: s.loopInsight,
+ loopCount: s.loopCount,
+ prestigeUpgrades: s.prestigeUpgrades,
+ memorySlots: s.memorySlots,
+ memories: s.memories,
+ pactSlots: s.pactSlots,
+ signedPacts: s.signedPacts,
+ defeatedGuardians: s.defeatedGuardians,
+ doPrestige: s.doPrestige,
+ })));
+
+ const startNewLoop = useGameStore((s) => s.startNewLoop);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ const handlePurchase = useCallback((id: string) => {
+ doPrestige(id);
+ }, [doPrestige]);
+
+ const handleResetLoop = useCallback(() => {
+ startNewLoop();
+ }, [startNewLoop]);
+
+ if (!mounted) {
+ return (
+
+ Loading prestige…
+
+ );
+ }
+
+ const upgradeEntries = Object.entries(PRESTIGE_DEF);
+
+ return (
+
+
+ {/* Insight & Loop Summary */}
+
+
+
+
+
{fmt(insight)}
+
Insight Available
+
+
+
{fmt(totalInsight)}
+
Total Insight Earned
+
+
+
{loopCount}
+
Loops Completed
+
+
+
{fmt(loopInsight)}
+
This Loop's Insight
+
+
+
+
+
+ {/* Memories & Pacts */}
+
+
+
+
+
+ Skills carried between loops. Slots: {memories.length}/{memorySlots}
+
+ {memories.length === 0 ? (
+ No memories stored yet.
+ ) : (
+
+ {memories.map((m) => (
+
+ {m.skillId}
+ Lv.{m.level} T{m.tier}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ Guardian pacts signed. Slots: {signedPacts.length}/{pactSlots}
+
+ {defeatedGuardians.length > 0 && (
+
+ Defeated guardians: {defeatedGuardians.map((f) => `F${f}`).join(', ')}
+
+ )}
+ {signedPacts.length === 0 ? (
+ No pacts signed yet.
+ ) : (
+
+ {signedPacts.map((f) => (
+
+ ✓ Floor {f} — Pact signed
+
+ ))}
+
+ )}
+
+
+
+
+ {/* Prestige Upgrades */}
+
+
+
+
+
+ {upgradeEntries.map(([id, def]) => (
+
+ ))}
+
+
+
+
+
+ {/* Reset Loop */}
+
+
+
+
+
Reset Loop
+
+ End the current loop and gain {fmt(loopInsight)} insight. Your prestige upgrades, memories, and pacts are preserved.
+
+
+
+
+
+ Reset Loop
+
+
+
+
+ Reset the Loop?
+
+ This will end your current loop and award you {fmt(loopInsight)} insight .
+ Your prestige upgrades, memories, and pacts will be preserved.
+
+ Day, hour, mana, floor progress, and combat state will be reset.
+
+
+
+ Cancel
+
+ Confirm Reset
+
+
+
+
+
+
+
+
+
+ );
+}
+
+PrestigeTab.displayName = 'PrestigeTab';
diff --git a/src/components/game/tabs/index.ts b/src/components/game/tabs/index.ts
index 8d0730d..df5ce8b 100644
--- a/src/components/game/tabs/index.ts
+++ b/src/components/game/tabs/index.ts
@@ -7,3 +7,4 @@ export { StatsTab } from './StatsTab';
export { DebugTab } from './DebugTab';
export { AchievementsTab } from './AchievementsTab';
export { AttunementsTab } from './AttunementsTab';
+export { PrestigeTab } from './PrestigeTab';