ce084a61a3
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
- GuardianPactsTab: extracted GuardianCard, PactHeaderSummary, TierFilter + 5 helper components into guardian-pacts-components.tsx - SpireSummaryTab: extracted TopStatsRow, NextGuardianCard, GuardianRoster, GuardianRosterItem, FloorLegend - PrestigeTab: extracted InsightSummary, MemoriesCard, PactsCard, ResetLoopSection - GameStateDebug: extracted WarningBanner, DisplayOptions, GameResetSection, ManaDebugSection, TimeControlSection, QuickActionsSection - EquipmentCrafter: extracted CraftingProgress, BlueprintCard, BlueprintList, MaterialCard, MaterialsInventory - PactDebug: extracted GuardianPactRow, GuardianPactList - GameStateDebugSection: extracted DisplayOptions, GameResetSection, ManaDebugSection, TimeControlSection, QuickActionsSection - PactDebugSection: extracted GuardianPactRow - SpireCombatPage: extracted useSpireStats hook - page.tsx: extracted GrimoireTab to separate file, useGameDerivedStats hook, TabTriggers, LazyTab wrapper All files now under 400 lines. Build passes. All 639 tests pass.
313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
'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';
|
|
|
|
// ─── Stat Cell ────────────────────────────────────────────────────────────────
|
|
|
|
function PrestigeStatCell({ value, label, color }: { value: string | number; label: string; color: string }) {
|
|
return (
|
|
<div className="text-center">
|
|
<div className={`text-2xl font-bold ${color}`}>{value}</div>
|
|
<div className="text-xs text-gray-400 mt-0.5">{label}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Insight Summary ──────────────────────────────────────────────────────────
|
|
|
|
function InsightSummary({ insight, totalInsight, loopCount, loopInsight }: {
|
|
insight: number;
|
|
totalInsight: number;
|
|
loopCount: number;
|
|
loopInsight: number;
|
|
}) {
|
|
return (
|
|
<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">
|
|
<PrestigeStatCell value={fmt(insight)} label="Insight Available" color="text-amber-400" />
|
|
<PrestigeStatCell value={fmt(totalInsight)} label="Total Insight Earned" color="text-gray-200" />
|
|
<PrestigeStatCell value={loopCount} label="Loops Completed" color="text-gray-200" />
|
|
<PrestigeStatCell value={fmt(loopInsight)} label="This Loop's Insight" color="text-purple-400" />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ─── Memories Card ────────────────────────────────────────────────────────────
|
|
|
|
function MemoriesCard({ memories, memorySlots }: { memories: { skillId: string; level: number; tier: number }[]; memorySlots: number }) {
|
|
return (
|
|
<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>
|
|
);
|
|
}
|
|
|
|
// ─── Pacts Card ───────────────────────────────────────────────────────────────
|
|
|
|
function PactsCard({ signedPacts, pactSlots, defeatedGuardians }: {
|
|
signedPacts: number[];
|
|
pactSlots: number;
|
|
defeatedGuardians: number[];
|
|
}) {
|
|
return (
|
|
<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>
|
|
);
|
|
}
|
|
|
|
// ─── 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>
|
|
);
|
|
}
|
|
|
|
// ─── Reset Loop Section ──────────────────────────────────────────────────────
|
|
|
|
function ResetLoopSection({ loopInsight, onReset }: { loopInsight: number; onReset: () => void }) {
|
|
return (
|
|
<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={onReset}>
|
|
Confirm Reset
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</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">
|
|
<InsightSummary
|
|
insight={insight}
|
|
totalInsight={totalInsight}
|
|
loopCount={loopCount}
|
|
loopInsight={loopInsight}
|
|
/>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<MemoriesCard memories={memories} memorySlots={memorySlots} />
|
|
<PactsCard signedPacts={signedPacts} pactSlots={pactSlots} defeatedGuardians={defeatedGuardians} />
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<ResetLoopSection loopInsight={loopInsight} onReset={handleResetLoop} />
|
|
</div>
|
|
</DebugName>
|
|
);
|
|
}
|
|
|
|
PrestigeTab.displayName = 'PrestigeTab';
|