222 lines
7.3 KiB
TypeScript
222 lines
7.3 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect, useMemo } from 'react';
|
||
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 { useCraftingStore } from '@/lib/game/stores/craftingStore';
|
||
import { getGuardianForFloor, isGuardianFloor } from '@/lib/game/data/guardian-encounters';
|
||
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';
|
||
|
||
// ─── Derived Stats Hook ──────────────────────────────────────────────────────
|
||
|
||
function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstances: Record<string, string | null>, equipmentInstances: Record<string, import('@/lib/game/types').EquipmentInstance>) {
|
||
const disciplineEffects = computeDisciplineEffects();
|
||
|
||
const upgradeEffects = getUnifiedEffects({
|
||
skillUpgrades: {},
|
||
skillTiers: {},
|
||
equippedInstances,
|
||
equipmentInstances,
|
||
});
|
||
|
||
const maxMana = computeMaxMana({
|
||
skills: {},
|
||
prestigeUpgrades,
|
||
skillUpgrades: {},
|
||
skillTiers: {},
|
||
}, upgradeEffects, disciplineEffects);
|
||
|
||
const baseRegen = computeRegen({
|
||
skills: {},
|
||
prestigeUpgrades,
|
||
skillUpgrades: {},
|
||
skillTiers: {},
|
||
attunements: {},
|
||
}, upgradeEffects, disciplineEffects);
|
||
|
||
return { maxMana, baseRegen };
|
||
}
|
||
|
||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||
|
||
export function SpireCombatPage() {
|
||
const [mounted, setMounted] = useState(false);
|
||
const [roomsCleared, setRoomsCleared] = useState(0);
|
||
|
||
const {
|
||
currentFloor,
|
||
floorHP,
|
||
floorMaxHP,
|
||
castProgress,
|
||
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,
|
||
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]);
|
||
|
||
useEffect(() => {
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||
setMounted(true);
|
||
setRoomsCleared(0);
|
||
const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms);
|
||
setCurrentRoom(newRoom);
|
||
setAction('climb');
|
||
}, [currentFloor, totalRooms, setCurrentRoom, setAction]);
|
||
|
||
const _handleRoomCleared = () => {
|
||
const nextRoomIndex = roomsCleared + 1;
|
||
|
||
if (nextRoomIndex >= totalRooms) {
|
||
const wasGuardian = isGuardianFloor(currentFloor);
|
||
setClearedFloor(currentFloor, true);
|
||
|
||
if (wasGuardian) {
|
||
const guardian = getGuardianForFloor(currentFloor);
|
||
if (guardian) {
|
||
addActivityLog('enemy_defeated', `⚔️ ${guardian.name} defeated!`, {
|
||
enemyName: guardian.name,
|
||
floor: currentFloor,
|
||
});
|
||
}
|
||
}
|
||
|
||
addActivityLog('floor_cleared', `🏰 Floor ${currentFloor} cleared!`, {
|
||
floor: currentFloor,
|
||
});
|
||
|
||
const newFloor = currentFloor + 1;
|
||
const newTotalRooms = getRoomsForFloor(newFloor);
|
||
const newRoom = generateSpireFloorState(newFloor, 0, newTotalRooms);
|
||
|
||
setCurrentRoom(newRoom);
|
||
setFloorHP(floorMaxHP);
|
||
setClearedFloor(currentFloor, true);
|
||
setRoomsCleared(0);
|
||
} else {
|
||
const newRoom = generateSpireFloorState(currentFloor, nextRoomIndex, totalRooms);
|
||
setCurrentRoom(newRoom);
|
||
setRoomsCleared(nextRoomIndex);
|
||
}
|
||
};
|
||
|
||
const handleClimbUp = () => {
|
||
startClimbUp();
|
||
addActivityLog('floor_transition', `⬆️ Climbing to floor ${currentFloor + 1}...`);
|
||
};
|
||
|
||
const handleClimbDown = () => {
|
||
if (currentFloor <= 1) return;
|
||
startClimbDown();
|
||
climbDownFloor();
|
||
setRoomsCleared(0);
|
||
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
|
||
};
|
||
|
||
const handleExitSpire = () => {
|
||
exitSpireMode();
|
||
addActivityLog('floor_transition', '🚪 Exited the Spire.');
|
||
};
|
||
|
||
if (!mounted) {
|
||
return (
|
||
<div className="flex items-center justify-center min-h-screen text-gray-500">
|
||
Loading spire...
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-950 flex flex-col">
|
||
<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>
|
||
<div className="text-xs text-gray-500">
|
||
Floor {currentFloor} · Insight: {fmt(insight)}
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="flex-1 p-4 space-y-4 max-w-7xl mx-auto w-full">
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||
<div className="lg:col-span-2">
|
||
<SpireHeader
|
||
currentFloor={currentFloor}
|
||
floorHP={floorHP}
|
||
floorMaxHP={floorMaxHP}
|
||
roomsCleared={roomsCleared}
|
||
totalRooms={totalRooms}
|
||
onClimbUp={handleClimbUp}
|
||
onClimbDown={handleClimbDown}
|
||
onExitSpire={handleExitSpire}
|
||
isDescending={isDescending}
|
||
/>
|
||
</div>
|
||
<div>
|
||
<SpireManaDisplay maxMana={maxMana} effectiveRegen={baseRegen} />
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||
<div className="lg:col-span-2">
|
||
<RoomDisplay floorState={currentRoom} floor={currentFloor} />
|
||
</div>
|
||
<div>
|
||
<SpireCombatControls castProgress={castProgress} />
|
||
</div>
|
||
</div>
|
||
|
||
<SpireActivityLog activityLog={activityLog} maxEntries={20} />
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|