refactor: extract sub-components from monster functions (issue #99)
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:
2026-05-20 18:38:24 +02:00
parent 53b3a94725
commit ce084a61a3
15 changed files with 1765 additions and 1539 deletions
@@ -5,82 +5,21 @@ import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, useManaStore, usePrestigeStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { getUnifiedEffects } from '@/lib/game/effects';
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
import { useCraftingStore } from '@/lib/game/stores/craftingStore';
import { GUARDIANS } from '@/lib/game/constants';
import { getExtendedGuardian, isGuardianFloor } from '@/lib/game/data/guardian-encounters';
import { getRoomsForFloor, generateSpireFloorState, calcInsight } from '@/lib/game/utils/spire-utils';
import { getRoomsForFloor, generateSpireFloorState } from '@/lib/game/utils/spire-utils';
import { SpireHeader } from './SpireHeader';
import { RoomDisplay } from './RoomDisplay';
import { SpireCombatControls } from './SpireCombatControls';
import { SpireActivityLog } from './SpireActivityLog';
import { SpireManaDisplay } from './SpireManaDisplay';
export function SpireCombatPage() {
const [mounted, setMounted] = useState(false);
const [roomsCleared, setRoomsCleared] = useState(0);
// ─── Derived Stats Hook ──────────────────────────────────────────────────────
// Combat store
const {
currentFloor,
floorHP,
floorMaxHP,
castProgress,
clearedFloors,
isDescending,
currentRoom,
activityLog,
setCurrentRoom,
setFloorHP,
setClearedFloor,
climbDownFloor,
exitSpireMode,
startClimbUp,
startClimbDown,
addActivityLog,
processCombatTick,
setAction,
} = useCombatStore(useShallow((s) => ({
currentFloor: s.currentFloor,
floorHP: s.floorHP,
floorMaxHP: s.floorMaxHP,
castProgress: s.castProgress,
clearedFloors: s.clearedFloors,
isDescending: s.isDescending,
currentRoom: s.currentRoom,
activityLog: s.activityLog,
setCurrentRoom: s.setCurrentRoom,
setFloorHP: s.setFloorHP,
setClearedFloor: s.setClearedFloor,
climbDownFloor: s.climbDownFloor,
exitSpireMode: s.exitSpireMode,
startClimbUp: s.startClimbUp,
startClimbDown: s.startClimbDown,
addActivityLog: s.addActivityLog,
processCombatTick: s.processCombatTick,
setAction: s.setAction,
})));
// Mana store
const { rawMana, elements } = useManaStore(useShallow((s) => ({
rawMana: s.rawMana,
elements: s.elements,
})));
// Prestige store
const { prestigeUpgrades, insight } = usePrestigeStore(useShallow((s) => ({
prestigeUpgrades: s.prestigeUpgrades,
insight: s.insight,
})));
// Crafting store for equipment effects
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
// Discipline effects
function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstances: unknown[], equipmentInstances: unknown[]) {
const disciplineEffects = computeDisciplineEffects();
// Compute derived stats
const upgradeEffects = getUnifiedEffects({
skillUpgrades: {},
skillTiers: {},
@@ -103,10 +42,70 @@ export function SpireCombatPage() {
attunements: {},
}, upgradeEffects, disciplineEffects);
// Total rooms for current floor
return { maxMana, baseRegen };
}
// ─── Main Component ───────────────────────────────────────────────────────────
export function SpireCombatPage() {
const [mounted, setMounted] = useState(false);
const [roomsCleared, setRoomsCleared] = useState(0);
const {
currentFloor,
floorHP,
floorMaxHP,
castProgress,
clearedFloors,
isDescending,
currentRoom,
activityLog,
setCurrentRoom,
setFloorHP,
setClearedFloor,
climbDownFloor,
exitSpireMode,
startClimbUp,
startClimbDown,
addActivityLog,
setAction,
} = useCombatStore(useShallow((s) => ({
currentFloor: s.currentFloor,
floorHP: s.floorHP,
floorMaxHP: s.floorMaxHP,
castProgress: s.castProgress,
clearedFloors: s.clearedFloors,
isDescending: s.isDescending,
currentRoom: s.currentRoom,
activityLog: s.activityLog,
setCurrentRoom: s.setCurrentRoom,
setFloorHP: s.setFloorHP,
setClearedFloor: s.setClearedFloor,
climbDownFloor: s.climbDownFloor,
exitSpireMode: s.exitSpireMode,
startClimbUp: s.startClimbUp,
startClimbDown: s.startClimbDown,
addActivityLog: s.addActivityLog,
setAction: s.setAction,
})));
const { rawMana, elements } = useManaStore(useShallow((s) => ({
rawMana: s.rawMana,
elements: s.elements,
})));
const { prestigeUpgrades, insight } = usePrestigeStore(useShallow((s) => ({
prestigeUpgrades: s.prestigeUpgrades,
insight: s.insight,
})));
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const { maxMana, baseRegen } = useSpireStats(prestigeUpgrades, equippedInstances, equipmentInstances);
const totalRooms = useMemo(() => getRoomsForFloor(currentFloor), [currentFloor]);
// Initialize room on floor change
useEffect(() => {
setMounted(true);
setRoomsCleared(0);
@@ -115,12 +114,10 @@ export function SpireCombatPage() {
setAction('climb');
}, [currentFloor, totalRooms, setCurrentRoom, setAction]);
// Handle room/floor transitions
const handleRoomCleared = () => {
const nextRoomIndex = roomsCleared + 1;
if (nextRoomIndex >= totalRooms) {
// Floor cleared
const wasGuardian = isGuardianFloor(currentFloor);
setClearedFloor(currentFloor, true);
@@ -138,30 +135,26 @@ export function SpireCombatPage() {
floor: currentFloor,
});
// Auto-advance to next floor
const newFloor = currentFloor + 1;
const newTotalRooms = getRoomsForFloor(newFloor);
const newRoom = generateSpireFloorState(newFloor, 0, newTotalRooms);
setCurrentRoom(newRoom);
setFloorHP(floorMaxHP); // Reset HP for new floor
setFloorHP(floorMaxHP);
setClearedFloor(currentFloor, true);
setRoomsCleared(0);
} else {
// Next room on same floor
const newRoom = generateSpireFloorState(currentFloor, nextRoomIndex, totalRooms);
setCurrentRoom(newRoom);
setRoomsCleared(nextRoomIndex);
}
};
// Handle climb up
const handleClimbUp = () => {
startClimbUp();
addActivityLog('floor_transition', `⬆️ Climbing to floor ${currentFloor + 1}...`);
};
// Handle climb down
const handleClimbDown = () => {
if (currentFloor <= 1) return;
startClimbDown();
@@ -170,7 +163,6 @@ export function SpireCombatPage() {
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
};
// Handle exit spire
const handleExitSpire = () => {
exitSpireMode();
addActivityLog('floor_transition', '🚪 Exited the Spire.');
@@ -186,7 +178,6 @@ export function SpireCombatPage() {
return (
<div className="min-h-screen bg-gray-950 flex flex-col">
{/* Compact header */}
<header className="sticky top-0 z-50 bg-gray-900/95 border-b border-gray-800 px-4 py-2">
<div className="flex items-center justify-between">
<h1 className="text-lg font-bold text-amber-400 tracking-wider">🏔 SPIRE</h1>
@@ -196,9 +187,7 @@ export function SpireCombatPage() {
</div>
</header>
{/* Main content */}
<main className="flex-1 p-4 space-y-4 max-w-7xl mx-auto w-full">
{/* Top section: Header + Mana */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="lg:col-span-2">
<SpireHeader
@@ -218,7 +207,6 @@ export function SpireCombatPage() {
</div>
</div>
{/* Middle section: Room + Controls */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="lg:col-span-2">
<RoomDisplay floorState={currentRoom} floor={currentFloor} />
@@ -228,7 +216,6 @@ export function SpireCombatPage() {
</div>
</div>
{/* Bottom: Activity Log */}
<SpireActivityLog activityLog={activityLog} maxEntries={20} />
</main>
</div>