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 -1
View File
@@ -1,4 +1,4 @@
# Circular Dependencies
Generated: 2026-06-04T11:37:52.108Z
Generated: 2026-06-04T16:12:56.097Z
No circular dependencies found. ✅
+13 -1
View File
@@ -1,6 +1,6 @@
{
"_meta": {
"generated": "2026-06-04T11:37:49.892Z",
"generated": "2026-06-04T16:12:54.221Z",
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
},
@@ -226,6 +226,9 @@
"data/attunements.ts": [
"types.ts"
],
"data/conversion-costs.ts": [
"types.ts"
],
"data/crafting-recipes.ts": [],
"data/disciplines/base.ts": [
"types/disciplines.ts"
@@ -672,6 +675,7 @@
"stores/gameStore.ts": [
"constants.ts",
"data/attunements.ts",
"data/guardian-encounters.ts",
"effects.ts",
"effects/discipline-effects.ts",
"effects/upgrade-effects.types.ts",
@@ -691,7 +695,9 @@
"stores/tick-pipeline.ts",
"stores/uiStore.ts",
"types.ts",
"utils/conversion-rates.ts",
"utils/element-cap-bonus.ts",
"utils/element-distance.ts",
"utils/index.ts",
"utils/safe-persist.ts"
],
@@ -825,10 +831,16 @@
"types.ts",
"utils/mana-utils.ts"
],
"utils/conversion-rates.ts": [
"data/conversion-costs.ts",
"effects/discipline-effects.ts",
"utils/element-distance.ts"
],
"utils/discipline-math.ts": [
"types/disciplines.ts"
],
"utils/element-cap-bonus.ts": [],
"utils/element-distance.ts": [],
"utils/enemy-generator.ts": [
"types.ts",
"utils/enemy-utils.ts",
@@ -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}
+5 -3
View File
@@ -7,9 +7,11 @@ export type { RoomType } from '../types/game';
// - Every 5th floor (5, 15, 25, etc.) has a chance for special rooms
// - Other floors are combat with chance for swarm/speed
export const PUZZLE_ROOM_INTERVAL = 7; // Every 7 floors, chance for puzzle
export const SWARM_ROOM_CHANCE = 0.15; // 15% chance for swarm room
export const SPEED_ROOM_CHANCE = 0.10; // 10% chance for speed room
export const PUZZLE_ROOM_CHANCE = 0.20; // 20% chance for puzzle room on puzzle floors
// NOTE: These constants are used by the legacy room-utils.ts (test-only).
// The active spire system in spire-utils.ts uses its own internal probabilities.
export const SWARM_ROOM_CHANCE = 0.15; // 15% chance for swarm room (legacy)
export const SPEED_ROOM_CHANCE = 0.10; // 10% chance for speed room (legacy)
export const PUZZLE_ROOM_CHANCE = 0.20; // 20% chance for puzzle room on puzzle floors (legacy)
// Puzzle room definitions - themed around attunements
export const PUZZLE_ROOMS: Record<string, {
@@ -9,7 +9,7 @@ import { usePrestigeStore } from './prestigeStore';
import { useDisciplineStore } from './discipline-slice';
import { useManaStore } from './manaStore';
import { summonGolemsOnRoomEntry } from './golem-combat-actions';
import type { FloorState } from '../types';
type GetFn = () => CombatStore;
type SetFn = (state: Partial<CombatState>) => void;
@@ -253,7 +253,7 @@ export function onEnterLibraryRoom(get: GetFn, set: SetFn): void {
// ─── enterSpireMode (climbing spec §4.1) ──────────────────────────────────────
export function createEnterSpireMode(get: GetFn, set: SetFn, generateFloorState: (floor: number) => FloorState) {
export function createEnterSpireMode(get: GetFn, set: SetFn) {
return () => {
const prestigeStore = usePrestigeStore.getState();
const spireKey = prestigeStore.prestigeUpgrades.spireKey || 0;
+12 -7
View File
@@ -6,7 +6,7 @@ import { persist } from 'zustand/middleware';
import { createSafeStorage } from '../utils/safe-persist';
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType, ActiveGolem, EnemyState, EquipmentInstance } from '../types';
import { getFloorMaxHP } from '../utils';
import { generateFloorState } from '../utils/room-utils';
import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils';
import { addActivityLogEntry } from '../utils/activity-log';
import { processCombatTick, makeInitialSpells } from './combat-actions';
import { getGuardianForFloor } from '../data/guardian-encounters';
@@ -32,7 +32,7 @@ export const useCombatStore = create<CombatStore>()(
spireMode: false,
// Room system
currentRoom: generateFloorState(1),
currentRoom: generateSpireFloorState(1, 0, getRoomsForFloor(1, 1 * 12345)),
// Spire climbing state
clearedFloors: {},
@@ -164,9 +164,11 @@ export const useCombatStore = create<CombatStore>()(
set((s) => {
if (s.currentFloor <= 1) return s;
const newFloor = s.currentFloor - 1;
const rooms = getRoomsForFloor(newFloor, newFloor * 12345);
const newRoom = generateSpireFloorState(newFloor, 0, rooms);
return {
currentFloor: newFloor,
currentRoom: generateFloorState(newFloor),
currentRoom: newRoom,
floorMaxHP: getFloorMaxHP(newFloor),
floorHP: getFloorMaxHP(newFloor),
castProgress: 0,
@@ -175,7 +177,9 @@ export const useCombatStore = create<CombatStore>()(
},
exitSpireMode: () => {
set((s) => ({
set((s) => {
const rooms = getRoomsForFloor(s.exitFloor, s.exitFloor * 12345);
return {
spireMode: false,
currentAction: 'meditate',
climbDirection: null,
@@ -183,7 +187,7 @@ export const useCombatStore = create<CombatStore>()(
currentFloor: s.exitFloor,
floorHP: getFloorMaxHP(s.exitFloor),
floorMaxHP: getFloorMaxHP(s.exitFloor),
currentRoom: generateFloorState(s.exitFloor),
currentRoom: generateSpireFloorState(s.exitFloor, 0, rooms),
castProgress: 0,
clearedFloors: {},
clearedRooms: {},
@@ -194,7 +198,8 @@ export const useCombatStore = create<CombatStore>()(
roomsPerFloor: 1,
maxFloorReached: Math.max(s.maxFloorReached, 1),
golemancy: { ...s.golemancy, activeGolems: [], summonedGolems: [] },
}));
};
});
},
startClimbUp: () => set({ climbDirection: 'up', currentAction: 'climb' }),
@@ -239,7 +244,7 @@ export const useCombatStore = create<CombatStore>()(
}));
},
enterSpireMode: createEnterSpireMode(get, set, generateFloorState),
enterSpireMode: createEnterSpireMode(get, set),
learnSpell: (spellId: string) => {
set((state) => ({