From 40a50d34f4fb9c3eb3c7202e6d5d31bec8992b46 Mon Sep 17 00:00:00 2001 From: n8n-gitea Date: Thu, 4 Jun 2026 18:54:33 +0200 Subject: [PATCH] fix: combat room progression - replace legacy room-utils with spire-utils, align UI with store state --- docs/circular-deps.txt | 2 +- docs/dependency-graph.json | 14 ++- .../tabs/SpireCombatPage/SpireCombatPage.tsx | 92 +------------------ src/lib/game/constants/rooms.ts | 8 +- src/lib/game/stores/combat-descent-actions.ts | 4 +- src/lib/game/stores/combatStore.ts | 19 ++-- 6 files changed, 35 insertions(+), 104 deletions(-) diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index 5aec3f8..22715d5 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,4 +1,4 @@ # Circular Dependencies -Generated: 2026-06-04T11:37:52.108Z +Generated: 2026-06-04T16:12:56.097Z No circular dependencies found. ✅ diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index f546e64..edd1261 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -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", diff --git a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx index 870b83d..19a5fe0 100644 --- a/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx +++ b/src/components/game/tabs/SpireCombatPage/SpireCombatPage.tsx @@ -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, 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(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} diff --git a/src/lib/game/constants/rooms.ts b/src/lib/game/constants/rooms.ts index 1389953..5957cb1 100644 --- a/src/lib/game/constants/rooms.ts +++ b/src/lib/game/constants/rooms.ts @@ -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 CombatStore; type SetFn = (state: Partial) => 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; diff --git a/src/lib/game/stores/combatStore.ts b/src/lib/game/stores/combatStore.ts index 0e4824d..6a795fb 100644 --- a/src/lib/game/stores/combatStore.ts +++ b/src/lib/game/stores/combatStore.ts @@ -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()( 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()( 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()( }, 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()( 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()( 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()( })); }, - enterSpireMode: createEnterSpireMode(get, set, generateFloorState), + enterSpireMode: createEnterSpireMode(get, set), learnSpell: (spellId: string) => { set((state) => ({