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
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-06-04T11:37:52.108Z
|
||||
Generated: 2026-06-04T16:12:56.097Z
|
||||
|
||||
No circular dependencies found. ✅
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user