fix: prevent conversion system from auto-unlocking all mana types on first tick
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m30s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m30s
- Removed auto-unlock lines in gameStore.ts element regen loop and recovery room boost loop - Added unlocked guard to skip locked elements during conversion processing - Added regression test (bug-377) verifying only transference stays unlocked Fixes #377
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-12T07:45:32.728Z
|
Generated: 2026-06-12T08:05:45.261Z
|
||||||
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-12T07:45:30.561Z",
|
"generated": "2026-06-12T08:05:43.098Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── bug-352-golem-mana-wipe.test.ts
|
│ │ │ │ ├── bug-352-golem-mana-wipe.test.ts
|
||||||
│ │ │ │ ├── bug-353-preparation-mana.test.ts
|
│ │ │ │ ├── bug-353-preparation-mana.test.ts
|
||||||
│ │ │ │ ├── bug-354-unlock-attunement.test.ts
|
│ │ │ │ ├── bug-354-unlock-attunement.test.ts
|
||||||
|
│ │ │ │ ├── bug-377-mana-auto-unlock.test.ts
|
||||||
│ │ │ │ ├── bug-fixes.test.ts
|
│ │ │ │ ├── bug-fixes.test.ts
|
||||||
│ │ │ │ ├── combat-actions.test.ts
|
│ │ │ │ ├── combat-actions.test.ts
|
||||||
│ │ │ │ ├── combat-utils.test.ts
|
│ │ │ │ ├── combat-utils.test.ts
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* Regression test for Bug #377:
|
||||||
|
* "All 22 mana types auto-unlocked on first tick via conversion system"
|
||||||
|
*
|
||||||
|
* Expected: Only 'transference' should be unlocked at game start.
|
||||||
|
* The conversion system must NOT auto-unlock elements that have non-zero rates.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { useManaStore } from '../stores/manaStore';
|
||||||
|
import { resetAllStores, tickN } from './cross-module-helpers';
|
||||||
|
import { BASE_UNLOCKED_ELEMENTS } from '../constants/elements';
|
||||||
|
|
||||||
|
function getUnlockedElements(): string[] {
|
||||||
|
const elements = useManaStore.getState().elements;
|
||||||
|
return Object.entries(elements)
|
||||||
|
.filter(([, v]) => v.unlocked)
|
||||||
|
.map(([k]) => k);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Bug #377 — mana types must not auto-unlock on first tick', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetAllStores();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only transference is unlocked at game start', () => {
|
||||||
|
const unlocked = getUnlockedElements();
|
||||||
|
expect(unlocked).toEqual(BASE_UNLOCKED_ELEMENTS);
|
||||||
|
expect(unlocked).toHaveLength(1);
|
||||||
|
expect(unlocked).toContain('transference');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('running one tick does not unlock any new elements', () => {
|
||||||
|
const before = getUnlockedElements();
|
||||||
|
expect(before).toEqual(['transference']);
|
||||||
|
|
||||||
|
// Run a single game tick
|
||||||
|
tickN(1);
|
||||||
|
|
||||||
|
const after = getUnlockedElements();
|
||||||
|
expect(after).toEqual(['transference']);
|
||||||
|
expect(after).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('running multiple ticks does not unlock exotic elements', () => {
|
||||||
|
// Run 20 ticks (simulating ~4 seconds of game time)
|
||||||
|
tickN(20);
|
||||||
|
|
||||||
|
const unlocked = getUnlockedElements();
|
||||||
|
expect(unlocked).toEqual(['transference']);
|
||||||
|
|
||||||
|
// Explicitly verify exotic elements remain locked
|
||||||
|
const elements = useManaStore.getState().elements;
|
||||||
|
const exoticElements = ['crystal', 'stellar', 'void', 'soul', 'time', 'plasma'];
|
||||||
|
for (const elem of exoticElements) {
|
||||||
|
expect(elements[elem]?.unlocked).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('conversion rates for locked elements do not cause unlock after many ticks', () => {
|
||||||
|
// Run 50 ticks
|
||||||
|
tickN(50);
|
||||||
|
|
||||||
|
const unlocked = getUnlockedElements();
|
||||||
|
expect(unlocked).toEqual(['transference']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -207,8 +207,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
}
|
}
|
||||||
// Apply net element regen to pools: produced - drained (component consumption)
|
// Apply net element regen to pools: produced - drained (component consumption)
|
||||||
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
||||||
if (entry.paused || !elements[elem]) continue;
|
if (entry.paused || !elements[elem] || !elements[elem].unlocked) continue;
|
||||||
if (!elements[elem].unlocked) elements[elem] = { ...elements[elem], unlocked: true };
|
|
||||||
const netRate = elementRegen[elem];
|
const netRate = elementRegen[elem];
|
||||||
if (netRate === 0) continue;
|
if (netRate === 0) continue;
|
||||||
const delta = netRate * HOURS_PER_TICK;
|
const delta = netRate * HOURS_PER_TICK;
|
||||||
@@ -320,8 +319,7 @@ export const useGameStore = create<GameCoordinatorStore>()(
|
|||||||
const regenDelta = Math.max(0, netBoostedRegen - netRawRegen);
|
const regenDelta = Math.max(0, netBoostedRegen - netRawRegen);
|
||||||
rawMana = Math.min(rawMana + regenDelta * HOURS_PER_TICK, maxMana);
|
rawMana = Math.min(rawMana + regenDelta * HOURS_PER_TICK, maxMana);
|
||||||
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
for (const [elem, entry] of Object.entries(conversionResult.rates)) {
|
||||||
if (entry.paused || entry.finalRate <= 0 || !elements[elem]) continue;
|
if (entry.paused || entry.finalRate <= 0 || !elements[elem] || !elements[elem].unlocked) continue;
|
||||||
if (!elements[elem].unlocked) elements[elem] = { ...elements[elem], unlocked: true };
|
|
||||||
// Normal conversion already applied above; add only the 9× delta
|
// Normal conversion already applied above; add only the 9× delta
|
||||||
elements[elem] = { ...elements[elem], current: Math.min(elements[elem].max, elements[elem].current + entry.finalRate * 9 * HOURS_PER_TICK) };
|
elements[elem] = { ...elements[elem], current: Math.min(elements[elem].max, elements[elem].current + entry.finalRate * 9 * HOURS_PER_TICK) };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user