f6f6ef4379
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
- #249: Add missing getAllGuardianFloors import to SpireSummaryTab.tsx - #250/#252: Add useRef guard in SpireCombatPage useEffect to prevent infinite re-render loop - #251: Fix stale closure in PactDebugSection signAllPacts/forcePact — read signedPacts from store.getState() - #253: Fix DisciplineDebugSection handleAddXP to update totalXP and concurrentLimit - #252: Marked duplicate of #250
123 lines
6.1 KiB
TypeScript
123 lines
6.1 KiB
TypeScript
'use client';
|
|
|
|
import { useMemo } from 'react';
|
|
import { useShallow } from 'zustand/react/shallow';
|
|
import { useCombatStore, usePrestigeStore } from '@/lib/game/stores';
|
|
import { FLOOR_ELEM_CYCLE } from '@/lib/game/constants';
|
|
import { getGuardianForFloor, getAllGuardianFloors } from '@/lib/game/data/guardian-encounters';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
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 {
|
|
getCounterElement, getElementColor, fmtArmor, fmtShield, fmtBarrier, fmtRegen,
|
|
PreparationTips, GuardianRoster, FloorProgressBar,
|
|
} from './SpireSummaryTab.helpers';
|
|
|
|
// ─── Top Stats Row ───────────────────────────────────────────────────────────
|
|
|
|
function TopStatsRow({ maxFloorReached, totalFloorsCleared, defeatedCount, insight }: {
|
|
maxFloorReached: number; totalFloorsCleared: number; defeatedCount: number; insight: 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">
|
|
<StatCell value={maxFloorReached} label="Max Floor Reached" color="text-amber-400" />
|
|
<StatCell value={totalFloorsCleared} label="Floors Cleared" color="text-gray-200" />
|
|
<StatCell value={defeatedCount} label="Guardians Defeated" color="text-emerald-400" />
|
|
<StatCell value={insight} label="Insight Earned" color="text-purple-400" isFmt />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function StatCell({ value, label, color, isFmt }: { value: number; label: string; color: string; isFmt?: boolean }) {
|
|
return (
|
|
<div className="text-center">
|
|
<div className={`text-2xl font-bold ${color}`}>{isFmt ? value : value}</div>
|
|
<div className="text-xs text-gray-400 mt-0.5">{label}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Next Guardian Card ──────────────────────────────────────────────────────
|
|
|
|
function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: number; nextGuardianData: ReturnType<typeof getGuardianForFloor> }) {
|
|
if (!nextGuardianData) return null;
|
|
const counterElement = getCounterElement(nextGuardianData.element[0]);
|
|
const nextFloorElement = FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length];
|
|
|
|
return (
|
|
<Card className="bg-gray-900/60 border-amber-800/40">
|
|
<SectionHeader title={`🛡️ Next Guardian — Floor ${nextGuardian}`} className="text-amber-400" />
|
|
<CardContent className="pt-0 space-y-3">
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className="w-10 h-10 rounded-full flex items-center justify-center text-lg font-bold"
|
|
style={{
|
|
backgroundColor: `${nextGuardianData.color}20`,
|
|
border: `2px solid ${nextGuardianData.color}`,
|
|
color: nextGuardianData.color,
|
|
}}
|
|
>
|
|
{nextGuardian}
|
|
</div>
|
|
<div>
|
|
<div className="font-semibold text-gray-100">{nextGuardianData.name}</div>
|
|
<div className="flex items-center gap-2 mt-0.5 flex-wrap">
|
|
<Badge variant="outline" className="text-xs" style={{ borderColor: getElementColor(nextGuardianData.element[0]), color: getElementColor(nextGuardianData.element[0]) }}>
|
|
{nextGuardianData.element.join(' + ')}
|
|
</Badge>
|
|
<span className="text-xs text-gray-500">Health: {nextGuardianData.hp}</span>
|
|
{fmtArmor(nextGuardianData.armor)}
|
|
{fmtShield(nextGuardianData.shield)}
|
|
{fmtBarrier(nextGuardianData.barrier)}
|
|
{fmtRegen(nextGuardianData.healthRegen, nextGuardianData.healthRegenIsPercent)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<PreparationTips counterElement={counterElement} nextFloorElement={nextFloorElement} hasHighArmor={!!(nextGuardianData.armor && nextGuardianData.armor > 0.15)} />
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// ─── Main Component ───────────────────────────────────────────────────────────
|
|
|
|
export function SpireSummaryTab() {
|
|
const { maxFloorReached, clearedFloors, enterSpireMode } = useCombatStore(useShallow((s) => ({
|
|
maxFloorReached: s.maxFloorReached,
|
|
clearedFloors: s.clearedFloors,
|
|
enterSpireMode: s.enterSpireMode,
|
|
})));
|
|
const { insight } = usePrestigeStore(useShallow((s) => ({ insight: s.insight })));
|
|
|
|
const defeatedGuardians = useMemo(() => getAllGuardianFloors().filter((floor) => clearedFloors[floor]), [clearedFloors]);
|
|
const nextGuardian = useMemo(() => getAllGuardianFloors().find((floor) => !clearedFloors[floor]) || null, [clearedFloors]);
|
|
const nextGuardianData = nextGuardian ? getGuardianForFloor(nextGuardian) : null;
|
|
const totalFloorsCleared = useMemo(() => Object.values(clearedFloors).filter(Boolean).length, [clearedFloors]);
|
|
|
|
return (
|
|
<DebugName name="SpireSummaryTab">
|
|
<div className="space-y-4">
|
|
<TopStatsRow maxFloorReached={maxFloorReached} totalFloorsCleared={totalFloorsCleared} defeatedCount={defeatedGuardians.length} insight={insight} />
|
|
{nextGuardianData && nextGuardian && <NextGuardianCard nextGuardian={nextGuardian} nextGuardianData={nextGuardianData} />}
|
|
<GuardianRoster clearedFloors={clearedFloors} />
|
|
<Card className="bg-gray-900/60 border-gray-700">
|
|
<SectionHeader title="🗺️ Floor Progress" />
|
|
<CardContent className="pt-0">
|
|
<ScrollArea className="h-[300px] pr-2">
|
|
<FloorProgressBar maxFloor={maxFloorReached} clearedFloors={clearedFloors} />
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</DebugName>
|
|
);
|
|
}
|
|
|
|
SpireSummaryTab.displayName = 'SpireSummaryTab';
|