[High] [Task] Combat room progression: UI bypasses store room state, legacy room-utils still imported #261
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Investigation Report: Combat & Floor Progression System — Floor HP vs Room HP
Summary
The combat and floor progression system has two parallel room generation systems with the legacy one (
room-utils.ts) partially active, creating dead code, stale constants, and a UI that doesn't use the store's room state. The core combat loop is fully wired and functional — it properly tracks per-enemy HP, applies damage per-enemy (focus-fire/AoE), and advances rooms viaadvanceRoomOrFloor. However, the UI component bypasses the store's room-aware state entirely, using its own independent room generation and a dead_handleRoomClearedcallback.System Map
Two Room Generation Systems
utils/room-utils.tsutils/spire-utils.tsKey Data Flow (Combat Loop)
State Fields
floorHPnumberfloorMaxHPnumbercurrentRoomFloorStatecurrentRoomIndexnumberroomsPerFloornumberclimbDirection'up' | 'down' | nullIntended Behavior (per spec & code structure)
startFloor(1 + spireKey × 2)advanceRoomOrFloor()→ next roomActual Behavior — Divergences Found
🔴 Bug #1: UI Bypasses Store Room State Entirely
File:
SpireCombatPage.tsxThe component maintains its own
roomsCleared(useState) andtotalRooms(useMemo) that are completely independent from the store'scurrentRoomIndexandroomsPerFloor. The_handleRoomClearedfunction is defined but never called (prefixed with_, not wired to any event).The
useEffecton lines 136-143 generates rooms using a different PRNG algorithm than the store:seed = (seed * 16807 + 0) % 2147483647(LCG)spire-utils.tsThis means the component can generate a different number of rooms than the store expects.
Impact: The UI shows room progress that may not match the store's authoritative state. The "Rooms Cleared" bar in SpireHeader and the "Room X / Y" badge in RoomDisplay can show different denominators.
🟡 Bug #2: Legacy
room-utils.tsStill Imported bycombatStore.tsFile:
combatStore.tsline 9This is used for:
currentRoomstate (line 37)climbDownFloor()(line 156)exitSpireMode()(line 170)createEnterSpireModeparameter (line 241)But the spire system in
combat-descent-actions.tsusesgenerateSpireFloorStatefromspire-utils.tsfor all room transitions. The legacygenerateFloorStategenerates a single room per floor with different room type probabilities and no seeded determinism.Impact: When
climbDownFloor()orexitSpireMode()is called, the room is generated by the legacy system, which may produce different room types than the spire system would.🟡 Bug #3: Stale Constants in
constants/rooms.tsThe constants
SWARM_ROOM_CHANCE = 0.15,SPEED_ROOM_CHANCE = 0.10,PUZZLE_ROOM_CHANCE = 0.20are used only by the legacyroom-utils.ts. The activespire-utils.tshas hardcoded different values (0.12, 0.10, forced-one-room).Impact: Misleading documentation; no runtime effect since the spire system doesn't import these constants.
🟡 Bug #4: Dead Code —
enemy-generator.tsutils/enemy-generator.tsis only imported by its test file. The spire system uses inline generation inspire-utils.ts.Impact: None at runtime; test-only dead code.
🟡 Bug #5: Dead Code —
guardian.shield/guardian.barriercombat-descent-actions.tslines 182-189 readsguardian.shieldandguardian.barrier, but theGuardianDeftype has no such fields. These always evaluate to 0.Impact: No runtime effect; guardian shield/barrier will always be 0.
🟢 Working Correctly: Core Combat Loop
The
processCombatTick→applyDamageToRoom→advanceRoomOrFloorchain is fully functional:Root Cause Analysis
The system appears to have undergone a partial migration from a single-room-per-floor model (
room-utils.ts) to a multi-room spire model (spire-utils.ts). The combat store and tick pipeline were fully migrated, but:combatStore.tsforclimbDownFloorandexitSpireMode_handleRoomClearedcallback was written but never wired — suggesting the migration was interruptedRelated Issues
combat-damage.ts)Recommendations
_handleRoomClearedto the combat tick's room-cleared event, or remove itgenerateFloorStateimports incombatStore.tswithgenerateSpireFloorStateconstants/rooms.tsto reflect actual spire percentagesenemy-generator.ts(or keep only the test)currentRoomIndex/roomsPerFloorinstead of independent computationguardian.shield/guardian.barriercode incombat-descent-actions.tsInvestigation Complete
Findings Summary:
The hypothesis that "combat resolves at the floor level, bypassing rooms" is partially correct but nuanced:
What's Working ✅
The core combat loop is fully room-aware:
processCombatTick→applyDamageToRoom()→ per-enemy HP tracking →advanceRoomOrFloor()currentRoomIndex,roomsPerFloor,currentRoom(FloorState with enemies[])What's Broken 🔴
SpireCombatPage.tsx) bypasses store room state — maintains independentroomsCleared/totalRoomsusing a different PRNG, creating potential divergence between UI display and store state_handleRoomClearedis defined but never called — dead callback suggesting interrupted migrationroom-utils.tsstill imported bycombatStore.tsforclimbDownFloor()andexitSpireMode(), generating rooms with wrong probabilitiesconstants/rooms.ts(15%/10%/20% vs actual 12%/10%/forced)enemy-generator.ts(test-only),guardian.shield/guardian.barrier(fields don't exist on type)Root Cause
Partial migration from single-room-per-floor (
room-utils.ts) to multi-room spire (spire-utils.ts). The combat store and tick pipeline were fully migrated, but the UI component and some store methods were not.Issue filed: #261
Starting work on combat room progression fixes. Will address: (1) UI bypassing store room state, (2) legacy room-utils imports, (3) stale constants, (4) dead code cleanup.
Fix Complete
All combat room progression issues resolved. Here's what was changed:
Files Modified (6)
stores/combatStore.ts— Replaced legacygenerateFloorStateimport fromroom-utils.tswithgenerateSpireFloorState+getRoomsForFloorfromspire-utils.ts. Updated initial state,climbDownFloor(), andexitSpireMode()to use spire room generation. RemovedgenerateFloorStateparameter fromcreateEnterSpireMode.stores/combat-descent-actions.ts— RemovedgenerateFloorStateparameter fromcreateEnterSpireMode()signature (was already usinggenerateSpireFloorStateinternally). Removed unusedFloorStateimport.constants/rooms.ts— Added documentation comments clarifying thatSWARM_ROOM_CHANCE,SPEED_ROOM_CHANCE, andPUZZLE_ROOM_CHANCEare legacy values used only by the test-onlyroom-utils.ts. The active spire system uses its own internal probabilities.components/.../SpireCombatPage.tsx— Removed independent room generation (roomsCleareduseState,seededRandom,totalRoomsuseMemo,useEffectroom generation, dead_handleRoomClearedcallback). Now uses store'scurrentRoomIndexandroomsPerFlooras the single source of truth for room progress display.docs/project-structure.txt&docs/dependency-graph.json— Auto-generated by pre-commit hook.Bugs Fixed
currentRoomIndex/roomsPerFloorfrom combat storeroom-utils.tsno longer imported bycombatStore.ts— replaced withspire-utils.tsenemy-generator.tsis test-only dead code (no action needed)Verification