// ─── Combat Descent Actions ──────────────────────────────────────────────────── // Extracted from combatStore.ts to keep the store under the 400-line limit. // Implements the spec-driven descent system (climbing spec §4.4–§4.9). import type { CombatState, CombatStore } from './combat-state.types'; import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils'; import { getGuardianForFloor } from '../data/guardian-encounters'; import { usePrestigeStore } from './prestigeStore'; import { useManaStore } from './manaStore'; import { summonGolemsOnRoomEntry } from './golem-combat-actions'; import { computeDisciplineEffects } from '../effects/discipline-effects'; import { useAttunementStore } from './attunementStore'; import { useDisciplineStore } from './discipline-slice'; import { onEnterLibraryRoom, onEnterRecoveryRoom, onEnterTreasureRoom, onEnterPuzzleRoom, } from './non-combat-room-actions'; type GetFn = () => CombatStore; type SetFn = (state: Partial) => void; /** Helper: compute total enemy HP from a room */ function calcRoomHP(room: { enemies?: Array<{ hp: number }> }): number { if (!room.enemies || room.enemies.length === 0) return 0; return room.enemies.reduce((sum, e) => sum + e.hp, 0); } // ─── enterDescentMode (climbing spec §4.5) ──────────────────────────────────── export function enterDescentMode(get: GetFn, set: SetFn): void { const s = get(); set({ climbDirection: 'down', descentPeak: { floor: s.currentFloor, roomIndex: s.currentRoomIndex }, isDescentComplete: false, runId: s.runId, }); get().addActivityLog('floor_transition', `Beginning descent from Floor ${s.currentFloor}, Room ${s.currentRoomIndex + 1}`); onEnterRoomDescend(get, set); } // ─── advanceRoomOrFloor (climbing spec §4.4, §4.6) ──────────────────────────── export function advanceRoomOrFloor(get: GetFn, set: SetFn): void { const s = get(); if (s.climbDirection === 'down') { get().addActivityLog('floor_transition', `Room ${s.currentRoomIndex + 1} passed`); if (s.currentFloor <= s.exitFloor && s.currentRoomIndex <= 0) { set({ isDescentComplete: true }); get().addActivityLog('floor_transition', 'Descent complete — Exit Spire is now available'); return; } if (s.currentRoomIndex <= 0) { const newFloor = s.currentFloor - 1; const seed = newFloor * 12345 + s.runId; const newRoomsPerFloor = getRoomsForFloor(newFloor, seed); const newRoomIndex = newRoomsPerFloor - 1; const newRoom = generateSpireFloorState(newFloor, newRoomIndex, newRoomsPerFloor, s.runId); const newFloorHP = calcRoomHP(newRoom); set({ currentFloor: newFloor, currentRoomIndex: newRoomIndex, roomsPerFloor: newRoomsPerFloor, currentRoom: newRoom, floorHP: newFloorHP, floorMaxHP: newFloorHP, castProgress: 0, weaponCastProgress: {}, }); get().addActivityLog('floor_transition', `Descended to Floor ${newFloor}`); } else { const newRoomIndex = s.currentRoomIndex - 1; const newRoom = generateSpireFloorState(s.currentFloor, newRoomIndex, s.roomsPerFloor, s.runId); const newFloorHP = calcRoomHP(newRoom); set({ currentRoomIndex: newRoomIndex, currentRoom: newRoom, floorHP: newFloorHP, floorMaxHP: newFloorHP, castProgress: 0, weaponCastProgress: {}, }); } summonGolemsForRoom(get, set); onEnterRoomDescend(get, set); } else { const roomKey = `${s.currentFloor}:${s.currentRoomIndex}`; set((prev) => ({ clearedRooms: { ...prev.clearedRooms, [roomKey]: true }, })); get().addActivityLog('floor_transition', `Floor ${s.currentFloor} Room ${s.currentRoomIndex + 1}/${s.roomsPerFloor} cleared`); if (s.currentRoomIndex + 1 >= s.roomsPerFloor) { const newFloor = Math.min(s.currentFloor + 1, 100); const newRoomsPerFloor = getRoomsForFloor(newFloor, newFloor * 12345 + s.runId); const newRoom = generateSpireFloorState(newFloor, 0, newRoomsPerFloor, s.runId); const newFloorHP = calcRoomHP(newRoom); set({ currentFloor: newFloor, currentRoomIndex: 0, roomsPerFloor: newRoomsPerFloor, currentRoom: newRoom, floorHP: newFloorHP, floorMaxHP: newFloorHP, castProgress: 0, weaponCastProgress: {}, }); get().addActivityLog('floor_transition', `Ascending to Floor ${newFloor}`); } else { const newRoomIndex = s.currentRoomIndex + 1; const newRoom = generateSpireFloorState(s.currentFloor, newRoomIndex, s.roomsPerFloor, s.runId); const newFloorHP = calcRoomHP(newRoom); set({ currentRoomIndex: newRoomIndex, currentRoom: newRoom, floorHP: newFloorHP, floorMaxHP: newFloorHP, castProgress: 0, weaponCastProgress: {}, }); } summonGolemsForRoom(get, set); // Handle non-combat rooms on ascent — initialize progress, do NOT auto-advance const room = get().currentRoom; if (room.roomType === 'library') { onEnterLibraryRoom(get, set); } else if (room.roomType === 'recovery') { onEnterRecoveryRoom(get, set); } else if (room.roomType === 'treasure') { onEnterTreasureRoom(get, set); } else if (room.roomType === 'puzzle') { onEnterPuzzleRoom(get, set); } } } // ─── Golem Summoning on Room Entry (spec §9.3) ────────────────────────────── function summonGolemsForRoom(get: GetFn, set: SetFn): void { const s = get(); const manaState = useManaStore.getState(); const loadout = s.golemancy?.golemLoadout ?? []; if (loadout.length === 0) return; const attStore = useAttunementStore.getState(); const fabLevel = attStore.attunements?.fabricator?.level ?? 0; const discEffects = computeDisciplineEffects(); const discBonus = Math.floor(discEffects.bonuses.golemCapacity || 0); const currentActiveGolems = s.golemancy?.activeGolems ?? []; const summonResult = summonGolemsOnRoomEntry( loadout, manaState.rawMana, manaState.elements, s.currentFloor, currentActiveGolems, discBonus, fabLevel, ); if (summonResult.logMessages.length > 0) { summonResult.logMessages.forEach((msg) => get().addActivityLog('special_effect', msg)); } set({ golemancy: { ...s.golemancy, activeGolems: summonResult.activeGolems }, }); useManaStore.setState({ rawMana: summonResult.rawMana, elements: summonResult.elements, }); } // ─── onEnterRoomDescend (climbing spec §4.7) ────────────────────────────────── export function onEnterRoomDescend(get: GetFn, set: SetFn): void { const s = get(); const key = `${s.currentFloor}:${s.currentRoomIndex}`; if (s.roomResetState[key] === undefined) { set((prev) => ({ roomResetState: { ...prev.roomResetState, [key]: Math.random() < 0.5 }, })); } const wasCleared = s.clearedRooms[key] === true; if (!wasCleared) { get().addActivityLog('floor_transition', `Floor ${s.currentFloor} Room ${s.currentRoomIndex + 1} was not cleared — enemies present`); // Initialize guardian defensive state for uncleared guardian rooms if (s.currentRoom.roomType === 'guardian') { const guardian = getGuardianForFloor(s.currentFloor); if (guardian) { set({ guardianShield: guardian.shield ?? 0, guardianShieldMax: guardian.shield ?? 0, guardianBarrier: guardian.barrier ?? 0, guardianBarrierMax: guardian.barrier ?? 0, }); } } // Initialize non-combat rooms during descent so they have proper progress tracking if (s.currentRoom.roomType === 'library') { onEnterLibraryRoom(get, set); } else if (s.currentRoom.roomType === 'recovery') { onEnterRecoveryRoom(get, set); } else if (s.currentRoom.roomType === 'treasure') { onEnterTreasureRoom(get, set); } else if (s.currentRoom.roomType === 'puzzle') { onEnterPuzzleRoom(get, set); } return; } const didReset = get().roomResetState[key]; if (didReset) { const newRoom = generateSpireFloorState(s.currentFloor, s.currentRoomIndex, s.roomsPerFloor, s.runId); set({ currentRoom: newRoom, castProgress: 0, weaponCastProgress: {} }); get().addActivityLog('floor_transition', `Floor ${s.currentFloor} Room ${s.currentRoomIndex + 1} has reset — enemies respawned`); if (newRoom.roomType === 'guardian') { const guardian = getGuardianForFloor(s.currentFloor); if (guardian) { set({ guardianShield: guardian.shield ?? 0, guardianShieldMax: guardian.shield ?? 0, guardianBarrier: guardian.barrier ?? 0, guardianBarrierMax: guardian.barrier ?? 0, }); } } } else { advanceRoomOrFloor(get, set); } } // ─── enterSpireMode (climbing spec §4.1) ────────────────────────────────────── export function createEnterSpireMode(get: GetFn, set: SetFn) { return () => { const prestigeStore = usePrestigeStore.getState(); const spireKey = prestigeStore.prestigeUpgrades.spireKey || 0; const startFloor = 1 + (spireKey * 2); const runId = Math.floor(Math.random() * 2147483647); const seed = startFloor * 12345 + runId; const rooms = getRoomsForFloor(startFloor, seed); const freshRoom = generateSpireFloorState(startFloor, 0, rooms, runId); set({ spireMode: true, currentAction: 'climb', currentFloor: startFloor, startFloor, exitFloor: startFloor, runId, currentRoomIndex: 0, roomsPerFloor: rooms, floorHP: calcRoomHP(freshRoom), floorMaxHP: calcRoomHP(freshRoom), currentRoom: freshRoom, castProgress: 0, weaponCastProgress: {}, climbDirection: 'up', isDescending: false, clearedFloors: {}, clearedRooms: {}, roomResetState: {}, descentPeak: null, isDescentComplete: false, golemancy: { activeGolems: [], lastSummonFloor: 0, golemDesigns: get().golemancy?.golemDesigns ?? {}, golemLoadout: [] }, }); // Deactivate all active disciplines when entering the Spire useDisciplineStore.getState().deactivateAll(); get().addActivityLog('floor_transition', `Entered the Spire at Floor ${startFloor}`); }; }