fix: prevent non-combat room repeated rewards during descent (Issue #382)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m24s

- Wrap library XP grant in progress < required guard so completed rooms don't grant XP every tick
- Add early return in treasure room tick when progress >= required to skip loot processing
- Initialize non-combat rooms (library, treasure, recovery) during descent in onEnterRoomDescend
- Add regression test file: non-combat-room-reward-guards.test.ts (8 tests)
This commit is contained in:
2026-06-12 14:18:04 +02:00
parent 58181139d0
commit fde5911780
6 changed files with 395 additions and 27 deletions
@@ -214,6 +214,17 @@ export function onEnterRoomDescend(get: GetFn, set: SetFn): void {
});
}
}
// 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;
}
+27 -25
View File
@@ -126,25 +126,27 @@ function tickLibraryRoom(get: GetFn, set: SetFn, hours: number, advance: Advance
const progress = (room.libraryProgress || 0) + hours;
const required = room.libraryRequired || 1;
const BASE_LIBRARY_XP_PER_HOUR = 50;
const xpGrant = Math.floor(BASE_LIBRARY_XP_PER_HOUR * 25 * hours);
if (progress < required) {
const BASE_LIBRARY_XP_PER_HOUR = 50;
const xpGrant = Math.floor(BASE_LIBRARY_XP_PER_HOUR * 25 * hours);
if (xpGrant > 0) {
const disciplineStore = useDisciplineStore.getState();
const allDisciplines = disciplineStore.disciplines;
const entries = Object.entries(allDisciplines).filter(([, ds]) => ds && !ds.paused);
if (xpGrant > 0) {
const disciplineStore = useDisciplineStore.getState();
const allDisciplines = disciplineStore.disciplines;
const entries = Object.entries(allDisciplines).filter(([, ds]) => ds && !ds.paused);
if (entries.length > 0) {
const [targetId, targetDs] = entries[Math.floor(Math.random() * entries.length)];
useDisciplineStore.setState((prev) => ({
disciplines: {
...prev.disciplines,
[targetId]: { ...targetDs, xp: (targetDs?.xp || 0) + xpGrant },
},
totalXP: prev.totalXP + xpGrant,
}));
get().addActivityLog('special_effect',
`${targetDs?.id || targetId} gained ${xpGrant} XP from studying ancient tomes`);
if (entries.length > 0) {
const [targetId, targetDs] = entries[Math.floor(Math.random() * entries.length)];
useDisciplineStore.setState((prev) => ({
disciplines: {
...prev.disciplines,
[targetId]: { ...targetDs, xp: (targetDs?.xp || 0) + xpGrant },
},
totalXP: prev.totalXP + xpGrant,
}));
get().addActivityLog('special_effect',
`${targetDs?.id || targetId} gained ${xpGrant} XP from studying ancient tomes`);
}
}
}
@@ -180,6 +182,13 @@ function tickTreasureRoom(get: GetFn, set: SetFn, hours: number, advance: Advanc
const loot = room.treasureLoot || [];
const claimed = room.treasureLootClaimed || [];
if (progress >= required) {
set({ currentRoom: { ...room, treasureProgress: progress, treasureLootClaimed: claimed } });
get().addActivityLog('special_effect', `Treasure room looted — ${claimed.length} items recovered`);
advance(get, set);
return;
}
const thresholds = [0.10, 0.50, 0.95, 1.0];
const newlyClaimed: number[] = [];
const claimedSet = new Set(claimed);
@@ -228,14 +237,7 @@ function tickTreasureRoom(get: GetFn, set: SetFn, hours: number, advance: Advanc
}
}
if (progress >= required) {
const claimedCount = claimedSet.size;
set({ currentRoom: { ...room, treasureProgress: progress, treasureLootClaimed: Array.from(claimedSet) } });
get().addActivityLog('special_effect', `Treasure room looted — ${claimedCount} items recovered`);
advance(get, set);
} else {
set({ currentRoom: { ...room, treasureProgress: progress, treasureLootClaimed: Array.from(claimedSet) } });
}
set({ currentRoom: { ...room, treasureProgress: progress, treasureLootClaimed: Array.from(claimedSet) } });
}
function tickPuzzleRoom(get: GetFn, set: SetFn, hours: number, advance: AdvanceFn): void {