feat: recreate Prestige tab with insight upgrades, memories, pacts, and loop reset
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s

This commit is contained in:
2026-05-19 20:19:31 +02:00
parent 5643a4c145
commit 1cd612193d
7 changed files with 384 additions and 2 deletions
+272
View File
@@ -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 (
<Card className={`bg-gray-900/60 ${isMaxed ? 'border-amber-700/50' : 'border-gray-700'}`}>
<CardContent className="p-4 space-y-2">
<div className="flex items-center justify-between">
<span className="font-medium text-sm text-gray-100">{name}</span>
<Badge
variant="outline"
className={isMaxed ? 'border-amber-600 text-amber-400' : 'border-gray-600 text-gray-400'}
>
{currentLevel}/{max}
</Badge>
</div>
<p className="text-xs text-gray-400">{desc}</p>
<div className="flex items-center justify-between pt-1">
<span className="text-xs text-gray-500">
Cost: <span className={canAfford ? 'text-amber-400' : 'text-red-400'}>{fmt(cost)}</span> insight
</span>
<Button
size="sm"
disabled={disabled}
onClick={() => onPurchase(id)}
className="h-7 px-3 text-xs"
>
{isMaxed ? 'Maxed' : 'Buy'}
</Button>
</div>
</CardContent>
</Card>
);
}
// ─── 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 (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading prestige
</div>
);
}
const upgradeEntries = Object.entries(PRESTIGE_DEF);
return (
<DebugName name="PrestigeTab">
<div className="space-y-4">
{/* Insight & Loop Summary */}
<Card className="bg-gray-900/60 border-gray-700">
<CardContent className="py-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-amber-400">{fmt(insight)}</div>
<div className="text-xs text-gray-400 mt-0.5">Insight Available</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-200">{fmt(totalInsight)}</div>
<div className="text-xs text-gray-400 mt-0.5">Total Insight Earned</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-200">{loopCount}</div>
<div className="text-xs text-gray-400 mt-0.5">Loops Completed</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-400">{fmt(loopInsight)}</div>
<div className="text-xs text-gray-400 mt-0.5">This Loop&apos;s Insight</div>
</div>
</div>
</CardContent>
</Card>
{/* Memories & Pacts */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="🧠 Memories" />
<CardContent className="pt-0">
<p className="text-xs text-gray-400 mb-2">
Skills carried between loops. Slots: {memories.length}/{memorySlots}
</p>
{memories.length === 0 ? (
<p className="text-xs text-gray-500 italic">No memories stored yet.</p>
) : (
<div className="space-y-1">
{memories.map((m) => (
<div key={m.skillId} className="text-xs text-gray-300 flex justify-between">
<span>{m.skillId}</span>
<span className="text-gray-500">Lv.{m.level} T{m.tier}</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="📜 Pacts" />
<CardContent className="pt-0">
<p className="text-xs text-gray-400 mb-2">
Guardian pacts signed. Slots: {signedPacts.length}/{pactSlots}
</p>
{defeatedGuardians.length > 0 && (
<p className="text-xs text-gray-500 mb-1">
Defeated guardians: {defeatedGuardians.map((f) => `F${f}`).join(', ')}
</p>
)}
{signedPacts.length === 0 ? (
<p className="text-xs text-gray-500 italic">No pacts signed yet.</p>
) : (
<div className="space-y-1">
{signedPacts.map((f) => (
<div key={f} className="text-xs text-green-400">
Floor {f} Pact signed
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
{/* Prestige Upgrades */}
<Card className="bg-gray-900/60 border-gray-700">
<SectionHeader title="⬆️ Prestige Upgrades" />
<CardContent className="pt-0">
<ScrollArea className="h-[400px] pr-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{upgradeEntries.map(([id, def]) => (
<UpgradeCard
key={id}
id={id}
name={def.name}
desc={def.desc}
max={def.max}
cost={def.cost}
currentLevel={prestigeUpgrades[id] || 0}
insight={insight}
onPurchase={handlePurchase}
/>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
{/* Reset Loop */}
<Card className="bg-gray-900/60 border-red-900/50">
<CardContent className="py-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-red-400">Reset Loop</h3>
<p className="text-xs text-gray-400 mt-1">
End the current loop and gain {fmt(loopInsight)} insight. Your prestige upgrades, memories, and pacts are preserved.
</p>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" size="sm" className="flex-shrink-0 ml-4">
Reset Loop
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset the Loop?</AlertDialogTitle>
<AlertDialogDescription>
This will end your current loop and award you <strong className="text-amber-400">{fmt(loopInsight)} insight</strong>.
Your prestige upgrades, memories, and pacts will be preserved.
<br /><br />
Day, hour, mana, floor progress, and combat state will be reset.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetLoop}>
Confirm Reset
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</CardContent>
</Card>
</div>
</DebugName>
);
}
PrestigeTab.displayName = 'PrestigeTab';