feat: Recreate Spire Combat Page — full spire climbing experience
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
- Add guardian-encounters.ts: Extended guardian definitions for all mana types (compound, exotic, combo) with dynamic name generation - Add spire-utils.ts: Spire-specific utilities (room generation, enemy stat scaling, insight calculation) - Add enemy-generator.ts: Enemy generation with combinable modifiers (mage, shield, armored, swarm, agile) - Add SpireCombatPage/ directory with modular sub-components: - SpireHeader.tsx: Floor info, climb controls, exit button, HP/room progress bars - RoomDisplay.tsx: Current room info with enemies, barriers, armor, dodge stats - SpireCombatControls.tsx: Spell selection panel, golem status panel - SpireActivityLog.tsx: Combat activity log - SpireManaDisplay.tsx: Compact mana display with elemental pools - Modify page.tsx: Conditionally render SpireCombatPage when spireMode is true - Add comprehensive tests (49 tests) for spire utilities, guardian encounters, and enemy generation
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
'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 { 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 { 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);
|
||||
|
||||
// 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
|
||||
const disciplineStoreState = useDisciplineStore();
|
||||
const disciplineEffects = computeDisciplineEffects(disciplineStoreState as any);
|
||||
|
||||
// Compute derived stats
|
||||
const upgradeEffects = getUnifiedEffects({
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
equippedInstances,
|
||||
equipmentInstances,
|
||||
});
|
||||
|
||||
const maxMana = computeMaxMana({
|
||||
skills: {},
|
||||
prestigeUpgrades,
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
}, upgradeEffects as any, disciplineEffects);
|
||||
|
||||
const baseRegen = computeRegen({
|
||||
skills: {},
|
||||
prestigeUpgrades,
|
||||
skillUpgrades: {},
|
||||
skillTiers: {},
|
||||
attunements: {},
|
||||
}, upgradeEffects as any, disciplineEffects);
|
||||
|
||||
// Total rooms for current floor
|
||||
const totalRooms = useMemo(() => getRoomsForFloor(currentFloor), [currentFloor]);
|
||||
|
||||
// Initialize room on floor change
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
setRoomsCleared(0);
|
||||
const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms);
|
||||
setCurrentRoom(newRoom);
|
||||
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);
|
||||
|
||||
if (wasGuardian) {
|
||||
const guardian = GUARDIANS[currentFloor] || getExtendedGuardian(currentFloor);
|
||||
if (guardian) {
|
||||
addActivityLog('enemy_defeated', `⚔️ ${guardian.name} defeated!`, {
|
||||
enemyName: guardian.name,
|
||||
floor: currentFloor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
addActivityLog('floor_cleared', `🏰 Floor ${currentFloor} cleared!`, {
|
||||
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
|
||||
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();
|
||||
climbDownFloor();
|
||||
setRoomsCleared(0);
|
||||
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
|
||||
};
|
||||
|
||||
// Handle exit spire
|
||||
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">
|
||||
{/* 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>
|
||||
<div className="text-xs text-gray-500">
|
||||
Floor {currentFloor} · Insight: {fmt(insight)}
|
||||
</div>
|
||||
</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
|
||||
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>
|
||||
|
||||
{/* 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} />
|
||||
</div>
|
||||
<div>
|
||||
<SpireCombatControls castProgress={castProgress} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom: Activity Log */}
|
||||
<SpireActivityLog activityLog={activityLog} maxEntries={20} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user