fix: deduplicate PAUSED conversion log messages in tick pipeline (bug #337)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m17s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m17s
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# Circular Dependencies
|
||||
Generated: 2026-06-09T10:03:41.836Z
|
||||
Generated: 2026-06-09T12:49:08.595Z
|
||||
Found: 2 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||
|
||||
1. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"_meta": {
|
||||
"generated": "2026-06-09T10:03:39.823Z",
|
||||
"generated": "2026-06-09T12:49:06.615Z",
|
||||
"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."
|
||||
},
|
||||
|
||||
@@ -228,6 +228,7 @@ Mana-Loop/
|
||||
│ │ │ │ ├── melee-auto-attack.test.ts
|
||||
│ │ │ │ ├── melee-defense-bypass.test.ts
|
||||
│ │ │ │ ├── pact-utils.test.ts
|
||||
│ │ │ │ ├── paused-conversion-dedup.test.ts
|
||||
│ │ │ │ ├── persistence.test.ts
|
||||
│ │ │ │ ├── regression-fixes.test.ts
|
||||
│ │ │ │ ├── room-utils-floor-state.test.ts
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { useGameStore } from '../stores/gameStore';
|
||||
import { useManaStore, makeInitialElements } from '../stores/manaStore';
|
||||
import { useCombatStore } from '../stores/combatStore';
|
||||
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||
import { useUIStore } from '../stores/uiStore';
|
||||
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||
import { useAttunementStore } from '../stores/attunementStore';
|
||||
import { useCraftingStore } from '../stores/craftingStore';
|
||||
import { getFloorMaxHP } from '../utils';
|
||||
import { ELEMENTS } from '../constants';
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function resetAllStores() {
|
||||
useUIStore.setState({
|
||||
paused: false,
|
||||
gameOver: false,
|
||||
victory: false,
|
||||
logs: [],
|
||||
});
|
||||
|
||||
useGameStore.setState({
|
||||
day: 1,
|
||||
hour: 0,
|
||||
incursionStrength: 0,
|
||||
containmentWards: 0,
|
||||
initialized: true,
|
||||
});
|
||||
|
||||
useManaStore.setState({
|
||||
rawMana: 100,
|
||||
meditateTicks: 0,
|
||||
totalManaGathered: 0,
|
||||
elements: makeInitialElements(50, {}),
|
||||
});
|
||||
|
||||
useCombatStore.setState({
|
||||
currentFloor: 1,
|
||||
floorHP: getFloorMaxHP(1),
|
||||
floorMaxHP: getFloorMaxHP(1),
|
||||
maxFloorReached: 1,
|
||||
activeSpell: 'manaBolt',
|
||||
currentAction: 'meditate',
|
||||
castProgress: 0,
|
||||
spireMode: false,
|
||||
currentRoom: { roomType: 'combat', enemies: [] },
|
||||
clearedFloors: {},
|
||||
climbDirection: null,
|
||||
isDescending: false,
|
||||
golemancy: { activeGolems: [], lastSummonFloor: 0, golemDesigns: {}, golemLoadout: [] },
|
||||
equipmentSpellStates: [],
|
||||
comboHitCount: 0,
|
||||
floorHitCount: 0,
|
||||
spells: { manaBolt: { learned: true, level: 1, studyProgress: 0 } },
|
||||
activityLog: [],
|
||||
achievements: { unlocked: [], progress: {} },
|
||||
totalSpellsCast: 0,
|
||||
totalDamageDealt: 0,
|
||||
totalCraftsCompleted: 0,
|
||||
});
|
||||
|
||||
usePrestigeStore.setState({
|
||||
loopCount: 0,
|
||||
insight: 0,
|
||||
totalInsight: 0,
|
||||
loopInsight: 0,
|
||||
prestigeUpgrades: {},
|
||||
pactSlots: 1,
|
||||
defeatedGuardians: [],
|
||||
signedPacts: [],
|
||||
signedPactDetails: {},
|
||||
pactRitualFloor: null,
|
||||
pactRitualProgress: 0,
|
||||
});
|
||||
|
||||
useDisciplineStore.setState({
|
||||
disciplines: {},
|
||||
activeIds: [],
|
||||
concurrentLimit: 1,
|
||||
totalXP: 0,
|
||||
processedPerks: [],
|
||||
});
|
||||
|
||||
useAttunementStore.setState({
|
||||
attunements: {},
|
||||
});
|
||||
|
||||
useCraftingStore.setState({
|
||||
designProgress: null,
|
||||
designProgress2: null,
|
||||
preparationProgress: null,
|
||||
applicationProgress: null,
|
||||
equipmentCraftingProgress: null,
|
||||
enchantmentDesigns: [],
|
||||
unlockedEffects: [],
|
||||
equippedInstances: {},
|
||||
equipmentInstances: {},
|
||||
lootInventory: {
|
||||
materials: {},
|
||||
blueprints: [],
|
||||
},
|
||||
enchantmentSelection: {
|
||||
selectedEquipmentType: null,
|
||||
selectedEffects: [],
|
||||
designName: '',
|
||||
selectedDesign: null,
|
||||
selectedEquipmentInstance: null,
|
||||
},
|
||||
lastError: null,
|
||||
});
|
||||
}
|
||||
|
||||
function unlockAllElements() {
|
||||
const mana = useManaStore.getState();
|
||||
for (const elem of Object.keys(ELEMENTS)) {
|
||||
if (!mana.elements[elem]?.unlocked) {
|
||||
// Directly set unlocked (simulating debug "Unlock All")
|
||||
useManaStore.setState({
|
||||
elements: {
|
||||
...useManaStore.getState().elements,
|
||||
[elem]: { ...useManaStore.getState().elements[elem], unlocked: true, current: 0 },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function countPausedLogs(logs: string[]): number {
|
||||
return logs.filter(l => l.includes('⚠️ PAUSED')).length;
|
||||
}
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('Paused conversion log deduplication (bug #337)', () => {
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
});
|
||||
|
||||
it('should log each paused conversion only once across many ticks', () => {
|
||||
unlockAllElements();
|
||||
|
||||
// Run 50 ticks (10 seconds of game time at 200ms/tick)
|
||||
for (let i = 0; i < 50; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
|
||||
const logs = useUIStore.getState().logs;
|
||||
const pausedCount = countPausedLogs(logs);
|
||||
|
||||
// With 22 elements unlocked but only transference having regen,
|
||||
// many conversions will be paused. Before the fix, this would
|
||||
// produce 50+ duplicate messages per tick (1000+ total).
|
||||
// After the fix, each unique paused conversion should appear at most once.
|
||||
// The exact count depends on how many elements have conversion rates,
|
||||
// but it should be a small constant, not proportional to tick count.
|
||||
expect(pausedCount).toBeLessThan(30);
|
||||
});
|
||||
|
||||
it('should not produce more PAUSED logs after the first tick for the same state', () => {
|
||||
unlockAllElements();
|
||||
|
||||
// First tick: should log paused conversions
|
||||
useGameStore.getState().tick();
|
||||
const logsAfterFirst = useUIStore.getState().logs;
|
||||
const pausedAfterFirst = countPausedLogs(logsAfterFirst);
|
||||
|
||||
// Run 99 more ticks with unchanged state
|
||||
for (let i = 0; i < 99; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
const logsAfter100 = useUIStore.getState().logs;
|
||||
const pausedAfter100 = countPausedLogs(logsAfter100);
|
||||
|
||||
// The count should be identical — no new PAUSED messages after tick 1
|
||||
expect(pausedAfter100).toBe(pausedAfterFirst);
|
||||
});
|
||||
|
||||
it('should re-log a conversion if it un-pauses and re-pauses', () => {
|
||||
unlockAllElements();
|
||||
|
||||
// First tick: logs paused conversions
|
||||
useGameStore.getState().tick();
|
||||
const pausedAfterFirst = countPausedLogs(useUIStore.getState().logs);
|
||||
|
||||
// Give enough raw regen to un-pause some conversions
|
||||
useManaStore.setState({
|
||||
rawMana: 999999,
|
||||
});
|
||||
|
||||
// Run a tick — conversions should un-pause (loggedPausedConversions cleans up)
|
||||
useGameStore.getState().tick();
|
||||
|
||||
// Now set raw regen low again by draining raw mana to 0
|
||||
// (conversions will pause again due to insufficient regen)
|
||||
useManaStore.setState({
|
||||
rawMana: 0,
|
||||
});
|
||||
|
||||
// Run another tick — conversions should re-pause and re-log
|
||||
useGameStore.getState().tick();
|
||||
const pausedAfterRepause = countPausedLogs(useUIStore.getState().logs);
|
||||
|
||||
// Should have at least as many as before (re-paused conversions re-log)
|
||||
expect(pausedAfterRepause).toBeGreaterThanOrEqual(pausedAfterFirst);
|
||||
});
|
||||
|
||||
it('should produce zero PAUSED logs when all conversions are active', () => {
|
||||
// Only transference unlocked (default), with enough raw regen
|
||||
useManaStore.setState({
|
||||
rawMana: 999999,
|
||||
});
|
||||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
useGameStore.getState().tick();
|
||||
}
|
||||
|
||||
const pausedCount = countPausedLogs(useUIStore.getState().logs);
|
||||
expect(pausedCount).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,9 @@ import type { GameCoordinatorState } from './gameStore.types';
|
||||
import type { EnemyState } from '../types';
|
||||
import { applyEnemyDefenses as applyEnemyDefensesFromPipeline } from './pipelines/combat-tick';
|
||||
|
||||
// Track paused conversions already logged to avoid flooding the activity log every tick
|
||||
const loggedPausedConversions = new Set<string>();
|
||||
|
||||
export interface GameCoordinatorStore extends GameCoordinatorState {
|
||||
tick: () => void;
|
||||
resetGame: () => void;
|
||||
@@ -177,12 +180,22 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
||||
let rawMana = ctx.mana.rawMana;
|
||||
let elements = { ...ctx.mana.elements };
|
||||
|
||||
// Log paused conversions
|
||||
// Log paused conversions (only on state change to avoid flooding the log)
|
||||
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
||||
if (entry.paused && entry.pauseReason) {
|
||||
if (!loggedPausedConversions.has(elem)) {
|
||||
loggedPausedConversions.add(elem);
|
||||
addLog(`⚠️ PAUSED: ${elem} conversion — ${entry.pauseReason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clean up entries for conversions that are no longer paused
|
||||
for (const elem of loggedPausedConversions) {
|
||||
const rateEntry = conversionResult.rates[elem];
|
||||
if (!rateEntry || !rateEntry.paused) {
|
||||
loggedPausedConversions.delete(elem);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute per-element net regen: produced rate - drain from being used as component
|
||||
const elementRegen: Record<string, number> = {};
|
||||
|
||||
Reference in New Issue
Block a user