refactor: extract sub-components from monster functions (issue #99)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
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.
This commit is contained in:
@@ -45,7 +45,6 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
|
||||
const totalFloors = Math.min(maxFloor, 100);
|
||||
const clearedSet = new Set(Object.entries(clearedFloors).filter(([, v]) => v).map(([k]) => Number(k)));
|
||||
|
||||
// Group floors into rows of 10 for display
|
||||
const rows: number[][] = [];
|
||||
for (let i = 0; i < totalFloors; i += 10) {
|
||||
rows.push(Array.from({ length: 10 }, (_, j) => i + j + 1).filter((f) => f <= totalFloors));
|
||||
@@ -88,23 +87,219 @@ function FloorProgressBar({ maxFloor, clearedFloors }: { maxFloor: number; clear
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" />
|
||||
<span>Cleared</span>
|
||||
<FloorLegend />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FloorLegend() {
|
||||
return (
|
||||
<div className="flex items-center gap-3 mt-2 text-[10px] text-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-emerald-600/60 border border-gray-700" />
|
||||
<span>Cleared</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-gray-800 border border-gray-700" />
|
||||
<span>Uncleared</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-gray-800 border border-amber-500" />
|
||||
<span>Guardian</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-amber-600/60 border border-amber-400" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── 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={fmt(insight)} label="Insight Earned" color="text-purple-400" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-gray-800 border border-gray-700" />
|
||||
<span>Uncleared</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCell({ value, label, color }: { value: number | string; 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>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Next Guardian Card ──────────────────────────────────────────────────────
|
||||
|
||||
function NextGuardianCard({ nextGuardian, nextGuardianData }: { nextGuardian: number; nextGuardianData: (typeof GUARDIANS)[number] }) {
|
||||
const counterElement = getCounterElement(nextGuardianData.element);
|
||||
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">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
style={{ borderColor: getElementColor(nextGuardianData.element), color: getElementColor(nextGuardianData.element) }}
|
||||
>
|
||||
{nextGuardianData.element}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">HP: {fmt(nextGuardianData.hp)}</span>
|
||||
{nextGuardianData.armor && (
|
||||
<span className="text-xs text-gray-500">
|
||||
Armor: {Math.round(nextGuardianData.armor * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-gray-800 border border-amber-500" />
|
||||
<span>Guardian</span>
|
||||
|
||||
<PreparationTips
|
||||
counterElement={counterElement}
|
||||
nextFloorElement={nextFloorElement}
|
||||
hasHighArmor={!!(nextGuardianData.armor && nextGuardianData.armor > 0.15)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function PreparationTips({ counterElement, nextFloorElement, hasHighArmor }: { counterElement: string | null; nextFloorElement: string | null; hasHighArmor: boolean }) {
|
||||
return (
|
||||
<div className="bg-gray-800/50 rounded-lg p-3 space-y-2">
|
||||
<div className="text-xs font-medium text-gray-300">Recommended Preparation:</div>
|
||||
<div className="text-xs text-gray-400 space-y-1">
|
||||
{counterElement && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-emerald-400">⚡</span>
|
||||
<span>
|
||||
Use <span style={{ color: getElementColor(counterElement) }} className="font-medium">{counterElement}</span> spells for super effective damage (+50%)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{nextFloorElement && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-400">🔄</span>
|
||||
<span>
|
||||
Floor element: <span style={{ color: getElementColor(nextFloorElement) }} className="font-medium">{nextFloorElement}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{hasHighArmor && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-red-400">🛡️</span>
|
||||
<span>High armor — consider armor-piercing or raw damage spells</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-amber-400">💡</span>
|
||||
<span>Ensure mana pools are full before attempting</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-3 h-3 rounded bg-amber-600/60 border border-amber-400" />
|
||||
<span>Current</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Guardian Roster ─────────────────────────────────────────────────────────
|
||||
|
||||
function GuardianRoster({ clearedFloors }: { clearedFloors: Record<number, boolean> }) {
|
||||
return (
|
||||
<Card className="bg-gray-900/60 border-gray-700">
|
||||
<SectionHeader title="🏛️ Guardian Roster" />
|
||||
<CardContent className="pt-0">
|
||||
<div className="space-y-2">
|
||||
{GUARDIAN_FLOORS.map((floor) => (
|
||||
<GuardianRosterItem key={floor} floor={floor} guardian={GUARDIANS[floor]} isDefeated={!!clearedFloors[floor]} />
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; guardian: (typeof GUARDIANS)[number]; isDefeated: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-between p-2 rounded border ${
|
||||
isDefeated
|
||||
? 'bg-emerald-900/20 border-emerald-800/40'
|
||||
: 'bg-gray-800/40 border-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-7 h-7 rounded flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
backgroundColor: isDefeated ? `${guardian.color}30` : '#374151',
|
||||
color: isDefeated ? guardian.color : '#6B7280',
|
||||
}}
|
||||
>
|
||||
{floor}
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-sm font-medium ${isDefeated ? 'text-gray-100' : 'text-gray-400'}`}>
|
||||
{guardian.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
backgroundColor: `${guardian.color}15`,
|
||||
color: guardian.color,
|
||||
}}
|
||||
>
|
||||
{guardian.element}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-500">HP: {fmt(guardian.hp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{isDefeated ? (
|
||||
<Badge variant="outline" className="border-emerald-600 text-emerald-400 text-xs">
|
||||
✓ Defeated
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-gray-600 text-gray-500 text-xs">
|
||||
Undefeated
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -135,7 +330,6 @@ export function SpireSummaryTab() {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
// Derived data
|
||||
const defeatedGuardians = useMemo(() => {
|
||||
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
|
||||
}, [clearedFloors]);
|
||||
@@ -146,9 +340,6 @@ export function SpireSummaryTab() {
|
||||
|
||||
const nextGuardianData = nextGuardian ? GUARDIANS[nextGuardian] : null;
|
||||
|
||||
const counterElement = nextGuardianData ? getCounterElement(nextGuardianData.element) : null;
|
||||
const nextFloorElement = nextGuardian ? FLOOR_ELEM_CYCLE[(nextGuardian - 1) % FLOOR_ELEM_CYCLE.length] : null;
|
||||
|
||||
const totalFloorsCleared = useMemo(() => {
|
||||
return Object.values(clearedFloors).filter(Boolean).length;
|
||||
}, [clearedFloors]);
|
||||
@@ -164,31 +355,13 @@ export function SpireSummaryTab() {
|
||||
return (
|
||||
<DebugName name="SpireSummaryTab">
|
||||
<div className="space-y-4">
|
||||
{/* ── Top Stats Row ─────────────────────────────────────────────── */}
|
||||
<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">{maxFloorReached}</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">Max Floor Reached</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-gray-200">{totalFloorsCleared}</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">Floors Cleared</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-emerald-400">{defeatedGuardians.length}</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">Guardians Defeated</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-400">{fmt(insight)}</div>
|
||||
<div className="text-xs text-gray-400 mt-0.5">Insight Earned</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<TopStatsRow
|
||||
maxFloorReached={maxFloorReached}
|
||||
totalFloorsCleared={totalFloorsCleared}
|
||||
defeatedCount={defeatedGuardians.length}
|
||||
insight={insight}
|
||||
/>
|
||||
|
||||
{/* ── Climb the Spire Button ────────────────────────────────────── */}
|
||||
<DebugName name="ClimbSpireButton">
|
||||
<Button
|
||||
className="w-full bg-gradient-to-r from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-600 text-white"
|
||||
@@ -200,146 +373,12 @@ export function SpireSummaryTab() {
|
||||
</Button>
|
||||
</DebugName>
|
||||
|
||||
{/* ── Next Guardian + Preparation ───────────────────────────────── */}
|
||||
{nextGuardianData && nextGuardian && (
|
||||
<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">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs"
|
||||
style={{ borderColor: getElementColor(nextGuardianData.element), color: getElementColor(nextGuardianData.element) }}
|
||||
>
|
||||
{nextGuardianData.element}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">HP: {fmt(nextGuardianData.hp)}</span>
|
||||
{nextGuardianData.armor && (
|
||||
<span className="text-xs text-gray-500">
|
||||
Armor: {Math.round(nextGuardianData.armor * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preparation recommendations */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-3 space-y-2">
|
||||
<div className="text-xs font-medium text-gray-300">Recommended Preparation:</div>
|
||||
<div className="text-xs text-gray-400 space-y-1">
|
||||
{counterElement && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-emerald-400">⚡</span>
|
||||
<span>
|
||||
Use <span style={{ color: getElementColor(counterElement) }} className="font-medium">{counterElement}</span> spells for super effective damage (+50%)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{nextFloorElement && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-blue-400">🔄</span>
|
||||
<span>
|
||||
Floor element: <span style={{ color: getElementColor(nextFloorElement) }} className="font-medium">{nextFloorElement}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{nextGuardianData.armor && nextGuardianData.armor > 0.15 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-red-400">🛡️</span>
|
||||
<span>High armor — consider armor-piercing or raw damage spells</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-amber-400">💡</span>
|
||||
<span>Ensure mana pools are full before attempting</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<NextGuardianCard nextGuardian={nextGuardian} nextGuardianData={nextGuardianData} />
|
||||
)}
|
||||
|
||||
{/* ── All Guardians List ────────────────────────────────────────── */}
|
||||
<Card className="bg-gray-900/60 border-gray-700">
|
||||
<SectionHeader title="🏛️ Guardian Roster" />
|
||||
<CardContent className="pt-0">
|
||||
<div className="space-y-2">
|
||||
{GUARDIAN_FLOORS.map((floor) => {
|
||||
const guardian = GUARDIANS[floor];
|
||||
const isDefeated = clearedFloors[floor];
|
||||
<GuardianRoster clearedFloors={clearedFloors} />
|
||||
|
||||
return (
|
||||
<div
|
||||
key={floor}
|
||||
className={`flex items-center justify-between p-2 rounded border ${
|
||||
isDefeated
|
||||
? 'bg-emerald-900/20 border-emerald-800/40'
|
||||
: 'bg-gray-800/40 border-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-7 h-7 rounded flex items-center justify-center text-xs font-bold"
|
||||
style={{
|
||||
backgroundColor: isDefeated ? `${guardian.color}30` : '#374151',
|
||||
color: isDefeated ? guardian.color : '#6B7280',
|
||||
}}
|
||||
>
|
||||
{floor}
|
||||
</div>
|
||||
<div>
|
||||
<div className={`text-sm font-medium ${isDefeated ? 'text-gray-100' : 'text-gray-400'}`}>
|
||||
{guardian.name}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span
|
||||
className="text-[10px] px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
backgroundColor: `${guardian.color}15`,
|
||||
color: guardian.color,
|
||||
}}
|
||||
>
|
||||
{guardian.element}
|
||||
</span>
|
||||
<span className="text-[10px] text-gray-500">HP: {fmt(guardian.hp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{isDefeated ? (
|
||||
<Badge variant="outline" className="border-emerald-600 text-emerald-400 text-xs">
|
||||
✓ Defeated
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="outline" className="border-gray-600 text-gray-500 text-xs">
|
||||
Undefeated
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── Floor Progress Map ────────────────────────────────────────── */}
|
||||
<Card className="bg-gray-900/60 border-gray-700">
|
||||
<SectionHeader title="🗺️ Floor Progress" />
|
||||
<CardContent className="pt-0">
|
||||
|
||||
Reference in New Issue
Block a user