fix: combat room progression - replace legacy room-utils with spire-utils, align UI with store state
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-04T11:37:52.108Z
|
Generated: 2026-06-04T16:12:56.097Z
|
||||||
|
|
||||||
No circular dependencies found. ✅
|
No circular dependencies found. ✅
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"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."
|
"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": [
|
"data/attunements.ts": [
|
||||||
"types.ts"
|
"types.ts"
|
||||||
],
|
],
|
||||||
|
"data/conversion-costs.ts": [
|
||||||
|
"types.ts"
|
||||||
|
],
|
||||||
"data/crafting-recipes.ts": [],
|
"data/crafting-recipes.ts": [],
|
||||||
"data/disciplines/base.ts": [
|
"data/disciplines/base.ts": [
|
||||||
"types/disciplines.ts"
|
"types/disciplines.ts"
|
||||||
@@ -672,6 +675,7 @@
|
|||||||
"stores/gameStore.ts": [
|
"stores/gameStore.ts": [
|
||||||
"constants.ts",
|
"constants.ts",
|
||||||
"data/attunements.ts",
|
"data/attunements.ts",
|
||||||
|
"data/guardian-encounters.ts",
|
||||||
"effects.ts",
|
"effects.ts",
|
||||||
"effects/discipline-effects.ts",
|
"effects/discipline-effects.ts",
|
||||||
"effects/upgrade-effects.types.ts",
|
"effects/upgrade-effects.types.ts",
|
||||||
@@ -691,7 +695,9 @@
|
|||||||
"stores/tick-pipeline.ts",
|
"stores/tick-pipeline.ts",
|
||||||
"stores/uiStore.ts",
|
"stores/uiStore.ts",
|
||||||
"types.ts",
|
"types.ts",
|
||||||
|
"utils/conversion-rates.ts",
|
||||||
"utils/element-cap-bonus.ts",
|
"utils/element-cap-bonus.ts",
|
||||||
|
"utils/element-distance.ts",
|
||||||
"utils/index.ts",
|
"utils/index.ts",
|
||||||
"utils/safe-persist.ts"
|
"utils/safe-persist.ts"
|
||||||
],
|
],
|
||||||
@@ -825,10 +831,16 @@
|
|||||||
"types.ts",
|
"types.ts",
|
||||||
"utils/mana-utils.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": [
|
"utils/discipline-math.ts": [
|
||||||
"types/disciplines.ts"
|
"types/disciplines.ts"
|
||||||
],
|
],
|
||||||
"utils/element-cap-bonus.ts": [],
|
"utils/element-cap-bonus.ts": [],
|
||||||
|
"utils/element-distance.ts": [],
|
||||||
"utils/enemy-generator.ts": [
|
"utils/enemy-generator.ts": [
|
||||||
"types.ts",
|
"types.ts",
|
||||||
"utils/enemy-utils.ts",
|
"utils/enemy-utils.ts",
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import { useCombatStore, useManaStore, usePrestigeStore, useGameStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores';
|
import { useCombatStore, useManaStore, usePrestigeStore, useGameStore, fmt, computeMaxMana, computeRegen } from '@/lib/game/stores';
|
||||||
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
|
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
|
||||||
import { getUnifiedEffects } from '@/lib/game/effects';
|
import { getUnifiedEffects } from '@/lib/game/effects';
|
||||||
import { useCraftingStore } from '@/lib/game/stores/craftingStore';
|
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 { SpireHeader } from './SpireHeader';
|
||||||
import { RoomDisplay } from './RoomDisplay';
|
import { RoomDisplay } from './RoomDisplay';
|
||||||
import { SpireCombatControls } from './SpireCombatControls';
|
import { SpireCombatControls } from './SpireCombatControls';
|
||||||
@@ -54,8 +51,6 @@ function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstanc
|
|||||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function SpireCombatPage() {
|
export function SpireCombatPage() {
|
||||||
const [roomsCleared, setRoomsCleared] = useState(0);
|
|
||||||
|
|
||||||
// ─── Spec: read room-aware state from combat store ───────────────────────
|
// ─── Spec: read room-aware state from combat store ───────────────────────
|
||||||
const {
|
const {
|
||||||
currentFloor,
|
currentFloor,
|
||||||
@@ -70,10 +65,6 @@ export function SpireCombatPage() {
|
|||||||
roomsPerFloor,
|
roomsPerFloor,
|
||||||
isDescentComplete,
|
isDescentComplete,
|
||||||
startFloor,
|
startFloor,
|
||||||
setCurrentRoom,
|
|
||||||
setFloorHP,
|
|
||||||
setClearedFloor,
|
|
||||||
climbDownFloor,
|
|
||||||
exitSpireMode,
|
exitSpireMode,
|
||||||
startClimbUp,
|
startClimbUp,
|
||||||
startClimbDown,
|
startClimbDown,
|
||||||
@@ -92,10 +83,6 @@ export function SpireCombatPage() {
|
|||||||
roomsPerFloor: s.roomsPerFloor,
|
roomsPerFloor: s.roomsPerFloor,
|
||||||
isDescentComplete: s.isDescentComplete,
|
isDescentComplete: s.isDescentComplete,
|
||||||
startFloor: s.startFloor,
|
startFloor: s.startFloor,
|
||||||
setCurrentRoom: s.setCurrentRoom,
|
|
||||||
setFloorHP: s.setFloorHP,
|
|
||||||
setClearedFloor: s.setClearedFloor,
|
|
||||||
climbDownFloor: s.climbDownFloor,
|
|
||||||
exitSpireMode: s.exitSpireMode,
|
exitSpireMode: s.exitSpireMode,
|
||||||
startClimbUp: s.startClimbUp,
|
startClimbUp: s.startClimbUp,
|
||||||
startClimbDown: s.startClimbDown,
|
startClimbDown: s.startClimbDown,
|
||||||
@@ -124,79 +111,6 @@ export function SpireCombatPage() {
|
|||||||
|
|
||||||
const { maxMana, baseRegen } = useSpireStats(prestigeUpgrades, equippedInstances, equipmentInstances);
|
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<string | null>(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 = () => {
|
const handleClimbUp = () => {
|
||||||
startClimbUp();
|
startClimbUp();
|
||||||
addActivityLog('floor_transition', `⬆️ Climbing to floor ${currentFloor + 1}...`);
|
addActivityLog('floor_transition', `⬆️ Climbing to floor ${currentFloor + 1}...`);
|
||||||
@@ -205,8 +119,6 @@ export function SpireCombatPage() {
|
|||||||
const handleClimbDown = () => {
|
const handleClimbDown = () => {
|
||||||
if (currentFloor <= 1) return;
|
if (currentFloor <= 1) return;
|
||||||
startClimbDown();
|
startClimbDown();
|
||||||
climbDownFloor();
|
|
||||||
setRoomsCleared(0);
|
|
||||||
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
|
addActivityLog('floor_transition', `⬇️ Descending to floor ${currentFloor - 1}...`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,8 +146,8 @@ export function SpireCombatPage() {
|
|||||||
currentFloor={currentFloor}
|
currentFloor={currentFloor}
|
||||||
floorHP={floorHP}
|
floorHP={floorHP}
|
||||||
floorMaxHP={floorMaxHP}
|
floorMaxHP={floorMaxHP}
|
||||||
roomsCleared={roomsCleared}
|
roomsCleared={currentRoomIndex}
|
||||||
totalRooms={totalRooms}
|
totalRooms={roomsPerFloor}
|
||||||
onClimbUp={handleClimbUp}
|
onClimbUp={handleClimbUp}
|
||||||
onClimbDown={handleClimbDown}
|
onClimbDown={handleClimbDown}
|
||||||
onExitSpire={handleExitSpire}
|
onExitSpire={handleExitSpire}
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ export type { RoomType } from '../types/game';
|
|||||||
// - Every 5th floor (5, 15, 25, etc.) has a chance for special rooms
|
// - Every 5th floor (5, 15, 25, etc.) has a chance for special rooms
|
||||||
// - Other floors are combat with chance for swarm/speed
|
// - Other floors are combat with chance for swarm/speed
|
||||||
export const PUZZLE_ROOM_INTERVAL = 7; // Every 7 floors, chance for puzzle
|
export const PUZZLE_ROOM_INTERVAL = 7; // Every 7 floors, chance for puzzle
|
||||||
export const SWARM_ROOM_CHANCE = 0.15; // 15% chance for swarm room
|
// NOTE: These constants are used by the legacy room-utils.ts (test-only).
|
||||||
export const SPEED_ROOM_CHANCE = 0.10; // 10% chance for speed room
|
// The active spire system in spire-utils.ts uses its own internal probabilities.
|
||||||
export const PUZZLE_ROOM_CHANCE = 0.20; // 20% chance for puzzle room on puzzle floors
|
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
|
// Puzzle room definitions - themed around attunements
|
||||||
export const PUZZLE_ROOMS: Record<string, {
|
export const PUZZLE_ROOMS: Record<string, {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { usePrestigeStore } from './prestigeStore';
|
|||||||
import { useDisciplineStore } from './discipline-slice';
|
import { useDisciplineStore } from './discipline-slice';
|
||||||
import { useManaStore } from './manaStore';
|
import { useManaStore } from './manaStore';
|
||||||
import { summonGolemsOnRoomEntry } from './golem-combat-actions';
|
import { summonGolemsOnRoomEntry } from './golem-combat-actions';
|
||||||
import type { FloorState } from '../types';
|
|
||||||
|
|
||||||
type GetFn = () => CombatStore;
|
type GetFn = () => CombatStore;
|
||||||
type SetFn = (state: Partial<CombatState>) => void;
|
type SetFn = (state: Partial<CombatState>) => void;
|
||||||
@@ -253,7 +253,7 @@ export function onEnterLibraryRoom(get: GetFn, set: SetFn): void {
|
|||||||
|
|
||||||
// ─── enterSpireMode (climbing spec §4.1) ──────────────────────────────────────
|
// ─── enterSpireMode (climbing spec §4.1) ──────────────────────────────────────
|
||||||
|
|
||||||
export function createEnterSpireMode(get: GetFn, set: SetFn, generateFloorState: (floor: number) => FloorState) {
|
export function createEnterSpireMode(get: GetFn, set: SetFn) {
|
||||||
return () => {
|
return () => {
|
||||||
const prestigeStore = usePrestigeStore.getState();
|
const prestigeStore = usePrestigeStore.getState();
|
||||||
const spireKey = prestigeStore.prestigeUpgrades.spireKey || 0;
|
const spireKey = prestigeStore.prestigeUpgrades.spireKey || 0;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { persist } from 'zustand/middleware';
|
|||||||
import { createSafeStorage } from '../utils/safe-persist';
|
import { createSafeStorage } from '../utils/safe-persist';
|
||||||
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType, ActiveGolem, EnemyState, EquipmentInstance } from '../types';
|
import type { GameAction, SpellState, FloorState, GolemancyState, ActivityLogEntry, AchievementState, EquipmentSpellState, ActivityEventType, ActiveGolem, EnemyState, EquipmentInstance } from '../types';
|
||||||
import { getFloorMaxHP } from '../utils';
|
import { getFloorMaxHP } from '../utils';
|
||||||
import { generateFloorState } from '../utils/room-utils';
|
import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils';
|
||||||
import { addActivityLogEntry } from '../utils/activity-log';
|
import { addActivityLogEntry } from '../utils/activity-log';
|
||||||
import { processCombatTick, makeInitialSpells } from './combat-actions';
|
import { processCombatTick, makeInitialSpells } from './combat-actions';
|
||||||
import { getGuardianForFloor } from '../data/guardian-encounters';
|
import { getGuardianForFloor } from '../data/guardian-encounters';
|
||||||
@@ -32,7 +32,7 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
spireMode: false,
|
spireMode: false,
|
||||||
|
|
||||||
// Room system
|
// Room system
|
||||||
currentRoom: generateFloorState(1),
|
currentRoom: generateSpireFloorState(1, 0, getRoomsForFloor(1, 1 * 12345)),
|
||||||
|
|
||||||
// Spire climbing state
|
// Spire climbing state
|
||||||
clearedFloors: {},
|
clearedFloors: {},
|
||||||
@@ -164,9 +164,11 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
set((s) => {
|
set((s) => {
|
||||||
if (s.currentFloor <= 1) return s;
|
if (s.currentFloor <= 1) return s;
|
||||||
const newFloor = s.currentFloor - 1;
|
const newFloor = s.currentFloor - 1;
|
||||||
|
const rooms = getRoomsForFloor(newFloor, newFloor * 12345);
|
||||||
|
const newRoom = generateSpireFloorState(newFloor, 0, rooms);
|
||||||
return {
|
return {
|
||||||
currentFloor: newFloor,
|
currentFloor: newFloor,
|
||||||
currentRoom: generateFloorState(newFloor),
|
currentRoom: newRoom,
|
||||||
floorMaxHP: getFloorMaxHP(newFloor),
|
floorMaxHP: getFloorMaxHP(newFloor),
|
||||||
floorHP: getFloorMaxHP(newFloor),
|
floorHP: getFloorMaxHP(newFloor),
|
||||||
castProgress: 0,
|
castProgress: 0,
|
||||||
@@ -175,7 +177,9 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
exitSpireMode: () => {
|
exitSpireMode: () => {
|
||||||
set((s) => ({
|
set((s) => {
|
||||||
|
const rooms = getRoomsForFloor(s.exitFloor, s.exitFloor * 12345);
|
||||||
|
return {
|
||||||
spireMode: false,
|
spireMode: false,
|
||||||
currentAction: 'meditate',
|
currentAction: 'meditate',
|
||||||
climbDirection: null,
|
climbDirection: null,
|
||||||
@@ -183,7 +187,7 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
currentFloor: s.exitFloor,
|
currentFloor: s.exitFloor,
|
||||||
floorHP: getFloorMaxHP(s.exitFloor),
|
floorHP: getFloorMaxHP(s.exitFloor),
|
||||||
floorMaxHP: getFloorMaxHP(s.exitFloor),
|
floorMaxHP: getFloorMaxHP(s.exitFloor),
|
||||||
currentRoom: generateFloorState(s.exitFloor),
|
currentRoom: generateSpireFloorState(s.exitFloor, 0, rooms),
|
||||||
castProgress: 0,
|
castProgress: 0,
|
||||||
clearedFloors: {},
|
clearedFloors: {},
|
||||||
clearedRooms: {},
|
clearedRooms: {},
|
||||||
@@ -194,7 +198,8 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
roomsPerFloor: 1,
|
roomsPerFloor: 1,
|
||||||
maxFloorReached: Math.max(s.maxFloorReached, 1),
|
maxFloorReached: Math.max(s.maxFloorReached, 1),
|
||||||
golemancy: { ...s.golemancy, activeGolems: [], summonedGolems: [] },
|
golemancy: { ...s.golemancy, activeGolems: [], summonedGolems: [] },
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
startClimbUp: () => set({ climbDirection: 'up', currentAction: 'climb' }),
|
startClimbUp: () => set({ climbDirection: 'up', currentAction: 'climb' }),
|
||||||
@@ -239,7 +244,7 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
enterSpireMode: createEnterSpireMode(get, set, generateFloorState),
|
enterSpireMode: createEnterSpireMode(get, set),
|
||||||
|
|
||||||
learnSpell: (spellId: string) => {
|
learnSpell: (spellId: string) => {
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user