feat: implement spire descent system with room-aware navigation
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
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:
@@ -7,11 +7,17 @@ import { createSafeStorage } from '../utils/safe-persist';
|
||||
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType } from '../types';
|
||||
import { getFloorMaxHP } from '../utils';
|
||||
import { generateFloorState } from '../utils/room-utils';
|
||||
import { generateSpireFloorState } from '../utils/spire-utils';
|
||||
import { addActivityLogEntry } from '../utils/activity-log';
|
||||
import { processCombatTick, makeInitialSpells } from './combat-actions';
|
||||
import { getGuardianForFloor } from '../data/guardian-encounters';
|
||||
import type { CombatStore } from './combat-state.types';
|
||||
import {
|
||||
enterDescentMode,
|
||||
advanceRoomOrFloor,
|
||||
onEnterRoomDescend,
|
||||
onEnterLibraryRoom,
|
||||
createEnterSpireMode,
|
||||
} from './combat-descent-actions';
|
||||
|
||||
export const useCombatStore = create<CombatStore>()(
|
||||
persist(
|
||||
@@ -33,6 +39,18 @@ export const useCombatStore = create<CombatStore>()(
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
|
||||
// ─── Spec: Room navigation state ──────────────────────────────────────
|
||||
startFloor: 1,
|
||||
exitFloor: 1,
|
||||
currentRoomIndex: 0,
|
||||
roomsPerFloor: 1,
|
||||
|
||||
// ─── Spec: Descent tracking state ─────────────────────────────────────
|
||||
descentPeak: null,
|
||||
roomResetState: {},
|
||||
clearedRooms: {},
|
||||
isDescentComplete: false,
|
||||
|
||||
// Golemancy
|
||||
golemancy: {
|
||||
enabledGolems: [],
|
||||
@@ -158,13 +176,18 @@ export const useCombatStore = create<CombatStore>()(
|
||||
currentAction: 'meditate',
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
currentFloor: 1,
|
||||
floorHP: getFloorMaxHP(1),
|
||||
floorMaxHP: getFloorMaxHP(1),
|
||||
currentRoom: generateFloorState(1),
|
||||
currentFloor: s.exitFloor,
|
||||
floorHP: getFloorMaxHP(s.exitFloor),
|
||||
floorMaxHP: getFloorMaxHP(s.exitFloor),
|
||||
currentRoom: generateFloorState(s.exitFloor),
|
||||
castProgress: 0,
|
||||
clearedFloors: {},
|
||||
// Preserve maxFloorReached — don't reset to 0 on spire exit (fix #238)
|
||||
clearedRooms: {},
|
||||
roomResetState: {},
|
||||
descentPeak: null,
|
||||
isDescentComplete: false,
|
||||
currentRoomIndex: 0,
|
||||
roomsPerFloor: 1,
|
||||
maxFloorReached: Math.max(s.maxFloorReached, 1),
|
||||
}));
|
||||
},
|
||||
@@ -174,17 +197,21 @@ export const useCombatStore = create<CombatStore>()(
|
||||
startClimbDown: () => set({ climbDirection: 'down', currentAction: 'climb' }),
|
||||
|
||||
startPracticing: () => set((s) => {
|
||||
// Only override if the current action is 'meditate' — don't clobber climb/study/etc.
|
||||
if (s.currentAction !== 'meditate') return s;
|
||||
return { currentAction: 'practicing' };
|
||||
}),
|
||||
|
||||
stopPracticing: () => set((s) => {
|
||||
// Only restore to meditate if we're currently practicing (don't clobber other actions)
|
||||
if (s.currentAction !== 'practicing') return s;
|
||||
return { currentAction: 'meditate' };
|
||||
}),
|
||||
|
||||
// ─── Spec: Descent actions (delegated to combat-descent-actions.ts) ────
|
||||
enterDescentMode: () => enterDescentMode(get, set),
|
||||
advanceRoomOrFloor: () => advanceRoomOrFloor(get, set),
|
||||
onEnterRoomDescend: () => onEnterRoomDescend(get, set),
|
||||
onEnterLibraryRoom: () => onEnterLibraryRoom(get, set),
|
||||
|
||||
// Golemancy
|
||||
toggleGolem: (golemId: string) => {
|
||||
set((s) => {
|
||||
@@ -207,22 +234,7 @@ export const useCombatStore = create<CombatStore>()(
|
||||
}));
|
||||
},
|
||||
|
||||
enterSpireMode: () => {
|
||||
const freshRoom = generateSpireFloorState(1, 0, 1);
|
||||
set((s) => ({
|
||||
spireMode: true,
|
||||
currentAction: 'climb',
|
||||
currentFloor: 1,
|
||||
floorHP: getFloorMaxHP(1),
|
||||
floorMaxHP: getFloorMaxHP(1),
|
||||
currentRoom: freshRoom,
|
||||
castProgress: 0,
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
clearedFloors: {},
|
||||
// Don't inflate maxFloorReached — it should reflect actual progress (fix #238)
|
||||
}));
|
||||
},
|
||||
enterSpireMode: createEnterSpireMode(get, set, generateFloorState),
|
||||
|
||||
learnSpell: (spellId: string) => {
|
||||
set((state) => ({
|
||||
|
||||
Reference in New Issue
Block a user