fix: add runId to seed calculations and use seeded random for treasure loot
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
Fixes #299: Seed calculation now includes runId component per spec (seed = floor × 12345 + runId) Fixes #298: Treasure loot now uses seeded random instead of Math.random() Changes: - Added runId field to CombatState type - Generated random runId on spire entry in createEnterSpireMode - Updated getRoomsForFloor, generateSpireRoomType, generateSpireFloorState, generateTreasureLoot to accept and use runId - Updated all call sites in combat-descent-actions.ts and combatStore.ts - Treasure loot item count now uses seeded RNG instead of Math.random()
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# 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.
|
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
|
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_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.",
|
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export function enterDescentMode(get: GetFn, set: SetFn): void {
|
|||||||
climbDirection: 'down',
|
climbDirection: 'down',
|
||||||
descentPeak: { floor: s.currentFloor, roomIndex: s.currentRoomIndex },
|
descentPeak: { floor: s.currentFloor, roomIndex: s.currentRoomIndex },
|
||||||
isDescentComplete: false,
|
isDescentComplete: false,
|
||||||
|
runId: s.runId,
|
||||||
});
|
});
|
||||||
get().addActivityLog('floor_transition',
|
get().addActivityLog('floor_transition',
|
||||||
`Beginning descent from Floor ${s.currentFloor}, Room ${s.currentRoomIndex + 1}`);
|
`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) {
|
if (s.currentRoomIndex <= 0) {
|
||||||
const newFloor = s.currentFloor - 1;
|
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 newRoomIndex = newRoomsPerFloor - 1;
|
||||||
const newRoom = generateSpireFloorState(newFloor, newRoomIndex, newRoomsPerFloor);
|
const newRoom = generateSpireFloorState(newFloor, newRoomIndex, newRoomsPerFloor, s.runId);
|
||||||
const newFloorHP = calcRoomHP(newRoom);
|
const newFloorHP = calcRoomHP(newRoom);
|
||||||
set({
|
set({
|
||||||
currentFloor: newFloor,
|
currentFloor: newFloor,
|
||||||
@@ -74,7 +76,7 @@ export function advanceRoomOrFloor(get: GetFn, set: SetFn): void {
|
|||||||
get().addActivityLog('floor_transition', `Descended to Floor ${newFloor}`);
|
get().addActivityLog('floor_transition', `Descended to Floor ${newFloor}`);
|
||||||
} else {
|
} else {
|
||||||
const newRoomIndex = s.currentRoomIndex - 1;
|
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);
|
const newFloorHP = calcRoomHP(newRoom);
|
||||||
set({
|
set({
|
||||||
currentRoomIndex: newRoomIndex,
|
currentRoomIndex: newRoomIndex,
|
||||||
@@ -98,7 +100,7 @@ export function advanceRoomOrFloor(get: GetFn, set: SetFn): void {
|
|||||||
if (s.currentRoomIndex + 1 >= s.roomsPerFloor) {
|
if (s.currentRoomIndex + 1 >= s.roomsPerFloor) {
|
||||||
const newFloor = Math.min(s.currentFloor + 1, 100);
|
const newFloor = Math.min(s.currentFloor + 1, 100);
|
||||||
const newRoomsPerFloor = getRoomsForFloor(newFloor, newFloor * 12345);
|
const newRoomsPerFloor = getRoomsForFloor(newFloor, newFloor * 12345);
|
||||||
const newRoom = generateSpireFloorState(newFloor, 0, newRoomsPerFloor);
|
const newRoom = generateSpireFloorState(newFloor, 0, newRoomsPerFloor, s.runId);
|
||||||
const newFloorHP = calcRoomHP(newRoom);
|
const newFloorHP = calcRoomHP(newRoom);
|
||||||
set({
|
set({
|
||||||
currentFloor: newFloor,
|
currentFloor: newFloor,
|
||||||
@@ -112,7 +114,7 @@ export function advanceRoomOrFloor(get: GetFn, set: SetFn): void {
|
|||||||
get().addActivityLog('floor_transition', `Ascending to Floor ${newFloor}`);
|
get().addActivityLog('floor_transition', `Ascending to Floor ${newFloor}`);
|
||||||
} else {
|
} else {
|
||||||
const newRoomIndex = s.currentRoomIndex + 1;
|
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);
|
const newFloorHP = calcRoomHP(newRoom);
|
||||||
set({
|
set({
|
||||||
currentRoomIndex: newRoomIndex,
|
currentRoomIndex: newRoomIndex,
|
||||||
@@ -213,7 +215,7 @@ export function onEnterRoomDescend(get: GetFn, set: SetFn): void {
|
|||||||
const didReset = get().roomResetState[key];
|
const didReset = get().roomResetState[key];
|
||||||
|
|
||||||
if (didReset) {
|
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 });
|
set({ currentRoom: newRoom, castProgress: 0 });
|
||||||
get().addActivityLog('floor_transition',
|
get().addActivityLog('floor_transition',
|
||||||
`Floor ${s.currentFloor} Room ${s.currentRoomIndex + 1} has reset — enemies respawned`);
|
`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 prestigeStore = usePrestigeStore.getState();
|
||||||
const spireKey = prestigeStore.prestigeUpgrades.spireKey || 0;
|
const spireKey = prestigeStore.prestigeUpgrades.spireKey || 0;
|
||||||
const startFloor = 1 + (spireKey * 2);
|
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 rooms = getRoomsForFloor(startFloor, seed);
|
||||||
const freshRoom = generateSpireFloorState(startFloor, 0, rooms);
|
const freshRoom = generateSpireFloorState(startFloor, 0, rooms, runId);
|
||||||
|
|
||||||
set({
|
set({
|
||||||
spireMode: true,
|
spireMode: true,
|
||||||
@@ -253,6 +256,7 @@ export function createEnterSpireMode(get: GetFn, set: SetFn) {
|
|||||||
currentFloor: startFloor,
|
currentFloor: startFloor,
|
||||||
startFloor,
|
startFloor,
|
||||||
exitFloor: startFloor,
|
exitFloor: startFloor,
|
||||||
|
runId,
|
||||||
currentRoomIndex: 0,
|
currentRoomIndex: 0,
|
||||||
roomsPerFloor: rooms,
|
roomsPerFloor: rooms,
|
||||||
floorHP: calcRoomHP(freshRoom),
|
floorHP: calcRoomHP(freshRoom),
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export interface CombatState {
|
|||||||
climbDirection: 'up' | 'down' | null;
|
climbDirection: 'up' | 'down' | null;
|
||||||
isDescending: boolean;
|
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) ───────────────────────────
|
// ─── Spec: Room navigation (climbing spec §6) ───────────────────────────
|
||||||
/** Floor the player entered at (= 1 + spireKey × 2) */
|
/** Floor the player entered at (= 1 + spireKey × 2) */
|
||||||
startFloor: number;
|
startFloor: number;
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
currentRoomIndex: 0,
|
currentRoomIndex: 0,
|
||||||
roomsPerFloor: 1,
|
roomsPerFloor: 1,
|
||||||
|
|
||||||
|
// ─── Spec: Run identity (climbing spec §4.2, §7) ────────────────────
|
||||||
|
runId: 0,
|
||||||
|
|
||||||
// ─── Spec: Descent tracking state ─────────────────────────────────────
|
// ─── Spec: Descent tracking state ─────────────────────────────────────
|
||||||
descentPeak: null,
|
descentPeak: null,
|
||||||
roomResetState: {},
|
roomResetState: {},
|
||||||
@@ -166,8 +169,9 @@ 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 seed = newFloor * 12345 + s.runId;
|
||||||
const newRoom = generateSpireFloorState(newFloor, 0, rooms);
|
const rooms = getRoomsForFloor(newFloor, seed);
|
||||||
|
const newRoom = generateSpireFloorState(newFloor, 0, rooms, s.runId);
|
||||||
return {
|
return {
|
||||||
currentFloor: newFloor,
|
currentFloor: newFloor,
|
||||||
currentRoom: newRoom,
|
currentRoom: newRoom,
|
||||||
@@ -180,7 +184,8 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
|
|
||||||
exitSpireMode: () => {
|
exitSpireMode: () => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const rooms = getRoomsForFloor(s.exitFloor, s.exitFloor * 12345);
|
const seed = s.exitFloor * 12345 + s.runId;
|
||||||
|
const rooms = getRoomsForFloor(s.exitFloor, seed);
|
||||||
return {
|
return {
|
||||||
spireMode: false,
|
spireMode: false,
|
||||||
currentAction: 'meditate',
|
currentAction: 'meditate',
|
||||||
@@ -189,7 +194,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: generateSpireFloorState(s.exitFloor, 0, rooms),
|
currentRoom: generateSpireFloorState(s.exitFloor, 0, rooms, s.runId),
|
||||||
castProgress: 0,
|
castProgress: 0,
|
||||||
clearedFloors: {},
|
clearedFloors: {},
|
||||||
clearedRooms: {},
|
clearedRooms: {},
|
||||||
@@ -362,6 +367,7 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
guardianBarrier: state.guardianBarrier,
|
guardianBarrier: state.guardianBarrier,
|
||||||
guardianBarrierMax: state.guardianBarrierMax,
|
guardianBarrierMax: state.guardianBarrierMax,
|
||||||
meleeSwordProgress: state.meleeSwordProgress,
|
meleeSwordProgress: state.meleeSwordProgress,
|
||||||
|
runId: state.runId,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,15 +39,13 @@ function makeSeededRandom(seed: number): () => number {
|
|||||||
// Deterministic per floor via seed = floor × 12345 + runId.
|
// Deterministic per floor via seed = floor × 12345 + runId.
|
||||||
// Guardian floors always return 1.
|
// 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;
|
if (isGuardianFloor(floor)) return 1;
|
||||||
const base = 5;
|
const base = 5;
|
||||||
const floorBonus = Math.min(10, Math.floor(floor / 20));
|
const floorBonus = Math.min(10, Math.floor(floor / 20));
|
||||||
|
|
||||||
// Use seeded random if a seed is provided, otherwise fall back to Math.random
|
// Use seeded random; seed should be floor × 12345 + runId per spec
|
||||||
const randomVariation = seed !== undefined
|
const randomVariation = Math.floor(makeSeededRandom(seed)() * 3);
|
||||||
? Math.floor(makeSeededRandom(seed)() * 3)
|
|
||||||
: Math.floor(Math.random() * 3);
|
|
||||||
|
|
||||||
return base + floorBonus + randomVariation;
|
return base + floorBonus + randomVariation;
|
||||||
}
|
}
|
||||||
@@ -62,6 +60,7 @@ export function generateSpireRoomType(
|
|||||||
floor: number,
|
floor: number,
|
||||||
roomIndex: number,
|
roomIndex: number,
|
||||||
totalRooms: number,
|
totalRooms: number,
|
||||||
|
runId: number = 0,
|
||||||
): SpireRoomType {
|
): SpireRoomType {
|
||||||
// Last room on guardian floors is always guardian
|
// Last room on guardian floors is always guardian
|
||||||
if (isGuardianFloor(floor) && roomIndex === totalRooms - 1) {
|
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'
|
// Override: every 7th floor, one room (chosen by seed) is always 'puzzle'
|
||||||
if (floor % 7 === 0) {
|
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) {
|
if (roomIndex === puzzleIndex) {
|
||||||
return 'puzzle';
|
return 'puzzle';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base roll
|
// Base roll — seed includes runId for per-run variety
|
||||||
const roll = makeSeededRandom(floor * 1000 + roomIndex)();
|
const roll = makeSeededRandom(floor * 1000 + roomIndex + runId)();
|
||||||
|
|
||||||
if (roll < 0.10) {
|
if (roll < 0.10) {
|
||||||
// Rare roll — secondary roll determines sub-type
|
// 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.40) return 'recovery';
|
||||||
if (rareRoll < 0.70) return 'treasure';
|
if (rareRoll < 0.70) return 'treasure';
|
||||||
return 'library';
|
return 'library';
|
||||||
@@ -93,8 +92,8 @@ export function generateSpireRoomType(
|
|||||||
|
|
||||||
// ─── Floor State Generation ───────────────────────────────────────────────────
|
// ─── Floor State Generation ───────────────────────────────────────────────────
|
||||||
|
|
||||||
export function generateSpireFloorState(floor: number, roomIndex: number, totalRooms: number): FloorState {
|
export function generateSpireFloorState(floor: number, roomIndex: number, totalRooms: number, runId: number = 0): FloorState {
|
||||||
const roomType = generateSpireRoomType(floor, roomIndex, totalRooms);
|
const roomType = generateSpireRoomType(floor, roomIndex, totalRooms, runId);
|
||||||
const element = getFloorElement(floor);
|
const element = getFloorElement(floor);
|
||||||
const baseHP = getFloorMaxHP(floor);
|
const baseHP = getFloorMaxHP(floor);
|
||||||
|
|
||||||
@@ -131,7 +130,7 @@ export function generateSpireFloorState(floor: number, roomIndex: number, totalR
|
|||||||
case 'puzzle': {
|
case 'puzzle': {
|
||||||
const puzzleKeys = Object.keys(PUZZLE_ROOMS);
|
const puzzleKeys = Object.keys(PUZZLE_ROOMS);
|
||||||
const puzzleIdx = Math.floor(
|
const puzzleIdx = Math.floor(
|
||||||
makeSeededRandom(floor * 1000 + roomIndex + 5000)() * puzzleKeys.length,
|
makeSeededRandom(floor * 1000 + roomIndex + runId + 5000)() * puzzleKeys.length,
|
||||||
);
|
);
|
||||||
const selectedPuzzle = puzzleKeys[puzzleIdx];
|
const selectedPuzzle = puzzleKeys[puzzleIdx];
|
||||||
const puzzle = PUZZLE_ROOMS[selectedPuzzle];
|
const puzzle = PUZZLE_ROOMS[selectedPuzzle];
|
||||||
@@ -162,7 +161,7 @@ export function generateSpireFloorState(floor: number, roomIndex: number, totalR
|
|||||||
};
|
};
|
||||||
|
|
||||||
case 'treasure': {
|
case 'treasure': {
|
||||||
const loot = generateTreasureLoot(floor);
|
const loot = generateTreasureLoot(floor, runId);
|
||||||
return {
|
return {
|
||||||
roomType: 'treasure',
|
roomType: 'treasure',
|
||||||
enemies: [],
|
enemies: [],
|
||||||
@@ -279,18 +278,21 @@ export function calcInsight(floor: number, isGuardian: boolean): number {
|
|||||||
* Generate treasure loot for a treasure room based on floor.
|
* Generate treasure loot for a treasure room based on floor.
|
||||||
* Returns pre-generated loot drops that are progressively revealed.
|
* 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 available = getAvailableDrops(floor, false);
|
||||||
const drops: LootDrop[] = [];
|
const drops: LootDrop[] = [];
|
||||||
|
|
||||||
|
// Use seeded random for deterministic loot
|
||||||
|
const rng = makeSeededRandom(floor * 12345 + runId + 31337);
|
||||||
|
|
||||||
// Determine item count based on floor
|
// Determine item count based on floor
|
||||||
let itemCount: number;
|
let itemCount: number;
|
||||||
if (floor <= 10) {
|
if (floor <= 10) {
|
||||||
itemCount = 2 + Math.floor(Math.random() * 2); // 2-3
|
itemCount = 2 + Math.floor(rng() * 2); // 2-3
|
||||||
} else if (floor <= 50) {
|
} else if (floor <= 50) {
|
||||||
itemCount = 4 + Math.floor(Math.random() * 4); // 4-7
|
itemCount = 4 + Math.floor(rng() * 4); // 4-7
|
||||||
} else {
|
} else {
|
||||||
itemCount = 8 + Math.floor(Math.random() * 8); // 8-15
|
itemCount = 8 + Math.floor(rng() * 8); // 8-15
|
||||||
}
|
}
|
||||||
|
|
||||||
// Roll for each item
|
// Roll for each item
|
||||||
|
|||||||
Reference in New Issue
Block a user