diff --git a/docs/circular-deps.txt b/docs/circular-deps.txt index e3ecb0f..9580711 100644 --- a/docs/circular-deps.txt +++ b/docs/circular-deps.txt @@ -1,5 +1,5 @@ # Circular Dependencies -Generated: 2026-06-08T12:36:38.273Z +Generated: 2026-06-08T12:41:09.468Z Found: 1 circular chain(s) — these MUST be fixed before modifying involved files. 1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts diff --git a/docs/dependency-graph.json b/docs/dependency-graph.json index e5ef880..dd8223b 100644 --- a/docs/dependency-graph.json +++ b/docs/dependency-graph.json @@ -1,6 +1,6 @@ { "_meta": { - "generated": "2026-06-08T12:36:36.211Z", + "generated": "2026-06-08T12:41:07.359Z", "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." }, diff --git a/src/lib/game/stores/combat-descent-actions.ts b/src/lib/game/stores/combat-descent-actions.ts index 396e257..fb5e5c7 100644 --- a/src/lib/game/stores/combat-descent-actions.ts +++ b/src/lib/game/stores/combat-descent-actions.ts @@ -35,6 +35,7 @@ export function enterDescentMode(get: GetFn, set: SetFn): void { 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}`); @@ -58,9 +59,10 @@ export function advanceRoomOrFloor(get: GetFn, set: SetFn): void { if (s.currentRoomIndex <= 0) { const newFloor = s.currentFloor - 1; - const newRoomsPerFloor = getRoomsForFloor(newFloor, newFloor * 12345); + const seed = newFloor * 12345 + s.runId; + const newRoomsPerFloor = getRoomsForFloor(newFloor, seed); const newRoomIndex = newRoomsPerFloor - 1; - const newRoom = generateSpireFloorState(newFloor, newRoomIndex, newRoomsPerFloor); + const newRoom = generateSpireFloorState(newFloor, newRoomIndex, newRoomsPerFloor, s.runId); const newFloorHP = calcRoomHP(newRoom); set({ currentFloor: newFloor, @@ -74,7 +76,7 @@ export function advanceRoomOrFloor(get: GetFn, set: SetFn): void { get().addActivityLog('floor_transition', `Descended to Floor ${newFloor}`); } else { const newRoomIndex = s.currentRoomIndex - 1; - const newRoom = generateSpireFloorState(s.currentFloor, newRoomIndex, s.roomsPerFloor); + const newRoom = generateSpireFloorState(s.currentFloor, newRoomIndex, s.roomsPerFloor, s.runId); const newFloorHP = calcRoomHP(newRoom); set({ currentRoomIndex: newRoomIndex, @@ -98,7 +100,7 @@ export function advanceRoomOrFloor(get: GetFn, set: SetFn): void { if (s.currentRoomIndex + 1 >= s.roomsPerFloor) { const newFloor = Math.min(s.currentFloor + 1, 100); const newRoomsPerFloor = getRoomsForFloor(newFloor, newFloor * 12345); - const newRoom = generateSpireFloorState(newFloor, 0, newRoomsPerFloor); + const newRoom = generateSpireFloorState(newFloor, 0, newRoomsPerFloor, s.runId); const newFloorHP = calcRoomHP(newRoom); set({ currentFloor: newFloor, @@ -112,7 +114,7 @@ export function advanceRoomOrFloor(get: GetFn, set: SetFn): void { get().addActivityLog('floor_transition', `Ascending to Floor ${newFloor}`); } else { const newRoomIndex = s.currentRoomIndex + 1; - const newRoom = generateSpireFloorState(s.currentFloor, newRoomIndex, s.roomsPerFloor); + const newRoom = generateSpireFloorState(s.currentFloor, newRoomIndex, s.roomsPerFloor, s.runId); const newFloorHP = calcRoomHP(newRoom); set({ currentRoomIndex: newRoomIndex, @@ -213,7 +215,7 @@ export function onEnterRoomDescend(get: GetFn, set: SetFn): void { const didReset = get().roomResetState[key]; if (didReset) { - const newRoom = generateSpireFloorState(s.currentFloor, s.currentRoomIndex, s.roomsPerFloor); + const newRoom = generateSpireFloorState(s.currentFloor, s.currentRoomIndex, s.roomsPerFloor, s.runId); set({ currentRoom: newRoom, castProgress: 0 }); get().addActivityLog('floor_transition', `Floor ${s.currentFloor} Room ${s.currentRoomIndex + 1} has reset — enemies respawned`); @@ -243,9 +245,10 @@ export function createEnterSpireMode(get: GetFn, set: SetFn) { const prestigeStore = usePrestigeStore.getState(); const spireKey = prestigeStore.prestigeUpgrades.spireKey || 0; const startFloor = 1 + (spireKey * 2); - const seed = startFloor * 12345; + const runId = Math.floor(Math.random() * 2147483647); + const seed = startFloor * 12345 + runId; const rooms = getRoomsForFloor(startFloor, seed); - const freshRoom = generateSpireFloorState(startFloor, 0, rooms); + const freshRoom = generateSpireFloorState(startFloor, 0, rooms, runId); set({ spireMode: true, @@ -253,6 +256,7 @@ export function createEnterSpireMode(get: GetFn, set: SetFn) { currentFloor: startFloor, startFloor, exitFloor: startFloor, + runId, currentRoomIndex: 0, roomsPerFloor: rooms, floorHP: calcRoomHP(freshRoom), diff --git a/src/lib/game/stores/combat-state.types.ts b/src/lib/game/stores/combat-state.types.ts index 47a5066..5019454 100644 --- a/src/lib/game/stores/combat-state.types.ts +++ b/src/lib/game/stores/combat-state.types.ts @@ -31,6 +31,10 @@ export interface CombatState { climbDirection: 'up' | 'down' | null; isDescending: boolean; + // ─── Spec: Run identity (climbing spec §4.2, §7) ────────────────────── + /** Unique run ID, generated on spire entry. Used as seed component: floor × 12345 + runId */ + runId: number; + // ─── Spec: Room navigation (climbing spec §6) ─────────────────────────── /** Floor the player entered at (= 1 + spireKey × 2) */ startFloor: number; diff --git a/src/lib/game/stores/combatStore.ts b/src/lib/game/stores/combatStore.ts index 47fa0b3..f22dd07 100644 --- a/src/lib/game/stores/combatStore.ts +++ b/src/lib/game/stores/combatStore.ts @@ -47,6 +47,9 @@ export const useCombatStore = create()( currentRoomIndex: 0, roomsPerFloor: 1, + // ─── Spec: Run identity (climbing spec §4.2, §7) ──────────────────── + runId: 0, + // ─── Spec: Descent tracking state ───────────────────────────────────── descentPeak: null, roomResetState: {}, @@ -166,8 +169,9 @@ export const useCombatStore = create()( set((s) => { if (s.currentFloor <= 1) return s; const newFloor = s.currentFloor - 1; - const rooms = getRoomsForFloor(newFloor, newFloor * 12345); - const newRoom = generateSpireFloorState(newFloor, 0, rooms); + const seed = newFloor * 12345 + s.runId; + const rooms = getRoomsForFloor(newFloor, seed); + const newRoom = generateSpireFloorState(newFloor, 0, rooms, s.runId); return { currentFloor: newFloor, currentRoom: newRoom, @@ -180,7 +184,8 @@ export const useCombatStore = create()( exitSpireMode: () => { set((s) => { - const rooms = getRoomsForFloor(s.exitFloor, s.exitFloor * 12345); + const seed = s.exitFloor * 12345 + s.runId; + const rooms = getRoomsForFloor(s.exitFloor, seed); return { spireMode: false, currentAction: 'meditate', @@ -189,7 +194,7 @@ export const useCombatStore = create()( currentFloor: s.exitFloor, floorHP: getFloorMaxHP(s.exitFloor), floorMaxHP: getFloorMaxHP(s.exitFloor), - currentRoom: generateSpireFloorState(s.exitFloor, 0, rooms), + currentRoom: generateSpireFloorState(s.exitFloor, 0, rooms, s.runId), castProgress: 0, clearedFloors: {}, clearedRooms: {}, @@ -362,6 +367,7 @@ export const useCombatStore = create()( guardianBarrier: state.guardianBarrier, guardianBarrierMax: state.guardianBarrierMax, meleeSwordProgress: state.meleeSwordProgress, + runId: state.runId, }), } ) diff --git a/src/lib/game/utils/spire-utils.ts b/src/lib/game/utils/spire-utils.ts index 12595eb..707ab15 100644 --- a/src/lib/game/utils/spire-utils.ts +++ b/src/lib/game/utils/spire-utils.ts @@ -39,15 +39,13 @@ function makeSeededRandom(seed: number): () => number { // Deterministic per floor via seed = floor × 12345 + runId. // Guardian floors always return 1. -export function getRoomsForFloor(floor: number, seed?: number): number { +export function getRoomsForFloor(floor: number, seed: number): number { if (isGuardianFloor(floor)) return 1; const base = 5; const floorBonus = Math.min(10, Math.floor(floor / 20)); - // Use seeded random if a seed is provided, otherwise fall back to Math.random - const randomVariation = seed !== undefined - ? Math.floor(makeSeededRandom(seed)() * 3) - : Math.floor(Math.random() * 3); + // Use seeded random; seed should be floor × 12345 + runId per spec + const randomVariation = Math.floor(makeSeededRandom(seed)() * 3); return base + floorBonus + randomVariation; } @@ -62,6 +60,7 @@ export function generateSpireRoomType( floor: number, roomIndex: number, totalRooms: number, + runId: number = 0, ): SpireRoomType { // Last room on guardian floors is always guardian if (isGuardianFloor(floor) && roomIndex === totalRooms - 1) { @@ -70,18 +69,18 @@ export function generateSpireRoomType( // Override: every 7th floor, one room (chosen by seed) is always 'puzzle' if (floor % 7 === 0) { - const puzzleIndex = Math.floor(makeSeededRandom(floor * 12345 + 7)() * totalRooms); + const puzzleIndex = Math.floor(makeSeededRandom(floor * 12345 + runId + 7)() * totalRooms); if (roomIndex === puzzleIndex) { return 'puzzle'; } } - // Base roll - const roll = makeSeededRandom(floor * 1000 + roomIndex)(); + // Base roll — seed includes runId for per-run variety + const roll = makeSeededRandom(floor * 1000 + roomIndex + runId)(); if (roll < 0.10) { // Rare roll — secondary roll determines sub-type - const rareRoll = makeSeededRandom(floor * 1000 + roomIndex + 9999)(); + const rareRoll = makeSeededRandom(floor * 1000 + roomIndex + runId + 9999)(); if (rareRoll < 0.40) return 'recovery'; if (rareRoll < 0.70) return 'treasure'; return 'library'; @@ -93,8 +92,8 @@ export function generateSpireRoomType( // ─── Floor State Generation ─────────────────────────────────────────────────── -export function generateSpireFloorState(floor: number, roomIndex: number, totalRooms: number): FloorState { - const roomType = generateSpireRoomType(floor, roomIndex, totalRooms); +export function generateSpireFloorState(floor: number, roomIndex: number, totalRooms: number, runId: number = 0): FloorState { + const roomType = generateSpireRoomType(floor, roomIndex, totalRooms, runId); const element = getFloorElement(floor); const baseHP = getFloorMaxHP(floor); @@ -131,7 +130,7 @@ export function generateSpireFloorState(floor: number, roomIndex: number, totalR case 'puzzle': { const puzzleKeys = Object.keys(PUZZLE_ROOMS); const puzzleIdx = Math.floor( - makeSeededRandom(floor * 1000 + roomIndex + 5000)() * puzzleKeys.length, + makeSeededRandom(floor * 1000 + roomIndex + runId + 5000)() * puzzleKeys.length, ); const selectedPuzzle = puzzleKeys[puzzleIdx]; const puzzle = PUZZLE_ROOMS[selectedPuzzle]; @@ -162,7 +161,7 @@ export function generateSpireFloorState(floor: number, roomIndex: number, totalR }; case 'treasure': { - const loot = generateTreasureLoot(floor); + const loot = generateTreasureLoot(floor, runId); return { roomType: 'treasure', enemies: [], @@ -279,18 +278,21 @@ export function calcInsight(floor: number, isGuardian: boolean): number { * Generate treasure loot for a treasure room based on floor. * Returns pre-generated loot drops that are progressively revealed. */ -export function generateTreasureLoot(floor: number): LootDrop[] { +export function generateTreasureLoot(floor: number, runId: number = 0): LootDrop[] { const available = getAvailableDrops(floor, false); const drops: LootDrop[] = []; + // Use seeded random for deterministic loot + const rng = makeSeededRandom(floor * 12345 + runId + 31337); + // Determine item count based on floor let itemCount: number; if (floor <= 10) { - itemCount = 2 + Math.floor(Math.random() * 2); // 2-3 + itemCount = 2 + Math.floor(rng() * 2); // 2-3 } else if (floor <= 50) { - itemCount = 4 + Math.floor(Math.random() * 4); // 4-7 + itemCount = 4 + Math.floor(rng() * 4); // 4-7 } else { - itemCount = 8 + Math.floor(Math.random() * 8); // 8-15 + itemCount = 8 + Math.floor(rng() * 8); // 8-15 } // Roll for each item