fix: combat room progression - replace legacy room-utils with spire-utils, align UI with store state
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s

This commit is contained in:
2026-06-04 18:54:33 +02:00
parent ab3afae2a6
commit 40a50d34f4
6 changed files with 35 additions and 104 deletions
@@ -1,13 +1,10 @@
'use client';
import { useState, useEffect, useMemo, useRef } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, useManaStore, usePrestigeStore, useGameStore, 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';
@@ -54,8 +51,6 @@ function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstanc
// ─── Main Component ───────────────────────────────────────────────────────────
export function SpireCombatPage() {
const [roomsCleared, setRoomsCleared] = useState(0);
// ─── Spec: read room-aware state from combat store ───────────────────────
const {
currentFloor,
@@ -70,10 +65,6 @@ export function SpireCombatPage() {
roomsPerFloor,
isDescentComplete,
startFloor,
setCurrentRoom,
setFloorHP,
setClearedFloor,
climbDownFloor,
exitSpireMode,
startClimbUp,
startClimbDown,
@@ -92,10 +83,6 @@ export function SpireCombatPage() {
roomsPerFloor: s.roomsPerFloor,
isDescentComplete: s.isDescentComplete,
startFloor: s.startFloor,
setCurrentRoom: s.setCurrentRoom,
setFloorHP: s.setFloorHP,
setClearedFloor: s.setClearedFloor,
climbDownFloor: s.climbDownFloor,
exitSpireMode: s.exitSpireMode,
startClimbUp: s.startClimbUp,
startClimbDown: s.startClimbDown,
@@ -124,79 +111,6 @@ export function SpireCombatPage() {
const { maxMana, baseRegen } = useSpireStats(prestigeUpgrades, equippedInstances, equipmentInstances);
// Use a deterministic seed based on floor to avoid Math.random() causing
// referential instability and infinite re-render loops.
const seededRandom = useMemo(() => {
let seed = currentFloor * 12345;
return () => {
seed = (seed * 16807 + 0) % 2147483647;
return (seed - 1) / 2147483646;
};
}, [currentFloor]);
const totalRooms = useMemo(() => {
if (isGuardianFloor(currentFloor)) return 1;
const base = 5;
const range = 10;
const floorBonus = Math.min(range, Math.floor(currentFloor / 20));
const randomVariation = Math.floor(seededRandom() * 3);
return base + floorBonus + randomVariation;
}, [currentFloor, seededRandom]);
// Generate initial room when floor or room count changes.
// Uses a ref guard to prevent infinite re-render loops.
const lastGeneratedRef = useRef<string | null>(null);
useEffect(() => {
const key = `${currentFloor}:${totalRooms}`;
if (lastGeneratedRef.current === key) return;
lastGeneratedRef.current = key;
setRoomsCleared(0);
const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms);
setCurrentRoom(newRoom);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentFloor, totalRooms]);
// Reset the generation guard when the component mounts
// (e.g. when re-entering spire mode after exit)
useEffect(() => {
lastGeneratedRef.current = null;
}, []);
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}...`);
@@ -205,8 +119,6 @@ export function SpireCombatPage() {
const handleClimbDown = () => {
if (currentFloor <= 1) return;
startClimbDown();
climbDownFloor();
setRoomsCleared(0);
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
};
@@ -234,8 +146,8 @@ export function SpireCombatPage() {
currentFloor={currentFloor}
floorHP={floorHP}
floorMaxHP={floorMaxHP}
roomsCleared={roomsCleared}
totalRooms={totalRooms}
roomsCleared={currentRoomIndex}
totalRooms={roomsPerFloor}
onClimbUp={handleClimbUp}
onClimbDown={handleClimbDown}
onExitSpire={handleExitSpire}