feat: implement spire descent system with room-aware navigation
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s

Implements the spec-driven spire multi-room climbing and descent system:
- Room navigation: currentRoomIndex, roomsPerFloor, startFloor, exitFloor
- Descent tracking: descentPeak, roomResetState, clearedRooms, isDescentComplete
- enterDescentMode: snapshots peak, sets climbDirection='down'
- advanceRoomOrFloor: room-by-room ascending/descending with floor transitions
- onEnterRoomDescend: per-room 50% reset check with auto-skip
- onEnterLibraryRoom: discipline XP scaled by floor
- Seeded PRNG for deterministic room counts and types
- UI: Descend button during ascent, Exit Spire only when isDescentComplete
- UI: Room X/Y display, room type badge, in-game time in RoomDisplay
- Extracted descent actions to combat-descent-actions.ts (file size limit)
- Updated tests for room-aware combat behavior

Spec: docs/specs/spire-climbing-spec.md §4.1-§4.9, §6
This commit is contained in:
2026-06-03 12:40:42 +02:00
parent feae6b468d
commit 1b4e5cf5ac
13 changed files with 638 additions and 126 deletions
+24 -4
View File
@@ -26,8 +26,16 @@ function resetStores() {
spireMode: false,
currentRoom: { roomType: 'combat', enemies: [] },
clearedFloors: {},
climbDirection: null,
climbDirection: 'up',
isDescending: false,
startFloor: 1,
exitFloor: 1,
currentRoomIndex: 0,
roomsPerFloor: 5,
descentPeak: null,
roomResetState: {},
clearedRooms: {},
isDescentComplete: false,
golemancy: { enabledGolems: [], summonedGolems: [], lastSummonFloor: 0 },
equipmentSpellStates: [],
comboHitCount: 0,
@@ -103,13 +111,25 @@ describe('processCombatTick', () => {
});
describe('floor advancement', () => {
it('should advance floor when HP reaches 0', () => {
it('should advance room when HP reaches 0 (not last room)', () => {
// Set floor HP very low so it's cleared in one cast
useCombatStore.setState({ floorHP: 1, castProgress: 0.99 });
// currentRoomIndex=0, roomsPerFloor=5 → clears room 0, advances to room 1
useCombatStore.setState({ floorHP: 1, castProgress: 0.99, climbDirection: 'up', currentRoomIndex: 0, roomsPerFloor: 5 });
const elements = makeInitialElements(500, {});
const result = runCombatTick(1000, elements);
// Room advanced, floor stays the same
expect(result.currentFloor).toBe(1);
// floorHP should be the new room's enemy HP
expect(result.floorHP).toBeGreaterThan(0);
});
it('should advance floor when last room on floor is cleared', () => {
// Set currentRoomIndex to last room so clearing it advances the floor
useCombatStore.setState({ floorHP: 1, castProgress: 0.99, climbDirection: 'up', currentRoomIndex: 4, roomsPerFloor: 5 });
const elements = makeInitialElements(500, {});
const result = runCombatTick(1000, elements);
expect(result.currentFloor).toBe(2);
expect(result.floorHP).toBe(getFloorMaxHP(2));
expect(result.floorHP).toBeGreaterThan(0);
});
it('should update maxFloorReached when advancing', () => {