From 1cd612193ddcffc9e67feaf31c190294c3cc1eda Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Tue, 19 May 2026 20:19:31 +0200 Subject: [PATCH] feat: recreate Prestige tab with insight upgrades, memories, pacts, and loop reset --- docs/circular-deps.txt | 2 +- docs/dependency-graph.json | 2 +- docs/project-structure.txt | 2 + src/app/page.tsx | 10 + src/components/game/tabs/PrestigeTab.test.ts | 97 +++++++ src/components/game/tabs/PrestigeTab.tsx | 272 +++++++++++++++++++ src/components/game/tabs/index.ts | 1 + 7 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 src/components/game/tabs/PrestigeTab.test.ts create mode 100644 src/components/game/tabs/PrestigeTab.tsx 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 + + +
+
+
+ ); +} + +// ─── 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 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';