fix: resolve 3 critical bugs — #354 attunement ReferenceError, #353 preparation mana exploit, #352 golem mana wipe
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m1s

- #354: unlockAttunement now uses _get() instead of undefined 'state' variable
- #353: startPreparing now deducts raw mana from the mana store after validation
- #352: processGolemAttacks/processBasicAttack accept current mana as params instead of initializing to 0/{}
- Updated golem-combat-actions.test.ts to pass new currentRawMana/currentElements params
- Added regression tests for all 3 bugs (16 new tests, all passing)
This commit is contained in:
2026-06-10 20:49:46 +02:00
parent 43bb53e0b4
commit 33be133813
12 changed files with 490 additions and 14 deletions
+2 -1
View File
@@ -80,7 +80,8 @@ export const useAttunementStore = create<AttunementStoreState>()(
unlockAttunement: (attunementId: string, defeatedGuardians: number[]) => {
const def = ATTUNEMENTS_DEF[attunementId];
if (!def) return false;
if (state.attunements[attunementId]?.active) return false;
const currentState = _get();
if (currentState.attunements[attunementId]?.active) return false;
// Check unlock conditions
if (attunementId === 'invoker') {
+2
View File
@@ -323,6 +323,8 @@ export function processCombatTick(
getFloorElement(currentFloor),
get,
set,
rawMana,
elements,
);
rawMana = golemResult.rawMana;
elements = golemResult.elements;
@@ -96,6 +96,8 @@ function runGolemAttacks(
() => enemy,
() => [enemy],
() => {},
100,
{},
);
return { result, capturedDamage };
}
@@ -136,7 +138,9 @@ describe('processGolemAttacks - spell damage and mana cost (fixes #1, #2)', () =
() => enemy,
() => [enemy],
() => {},
);
100,
{},
);
expect(spellCastCount).toBeGreaterThan(0);
});
@@ -160,7 +164,9 @@ describe('processGolemAttacks - spell damage and mana cost (fixes #1, #2)', () =
() => enemy,
() => [enemy],
() => {},
);
100,
{},
);
// Should fall back to basic attack since mana is insufficient
expect(damageCount).toBeGreaterThan(0);
@@ -245,7 +251,9 @@ describe('processGolemAttacks - enchantment effects (fix #4)', () => {
() => enemy,
() => [enemy],
(enemyId, effects) => { appliedEffects.push({ enemyId, effects }); },
);
100,
{},
);
expect(appliedEffects.length).toBeGreaterThan(0);
expect(appliedEffects[0].effects.some(e => e.type === 'burn')).toBe(true);
@@ -267,7 +275,9 @@ describe('processGolemAttacks - enchantment effects (fix #4)', () => {
() => enemy,
() => [enemy],
(_, effects) => { appliedEffects.push(effects); },
);
100,
{},
);
const allEffectTypes = appliedEffects.flat().map(e => e.type);
expect(allEffectTypes).toContain('burn');
@@ -291,7 +301,9 @@ describe('processGolemAttacks - enchantment effects (fix #4)', () => {
() => enemy,
() => [enemy],
() => { effectsCalled = true; },
);
100,
{},
);
expect(effectsCalled).toBe(false);
});
+5 -3
View File
@@ -248,9 +248,11 @@ export function processGolemAttacks(
getTargetEnemy: () => EnemyState | null,
getTargetEnemies: () => EnemyState[],
onApplyEnchantmentEffects: (enemyId: string, effects: GolemEnchantmentEffect[]) => void,
currentRawMana: number,
currentElements: Record<string, { current: number; max: number; unlocked: boolean }>,
): GolemCombatResult {
let rawMana = 0;
let elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
let rawMana = currentRawMana;
let elements: Record<string, { current: number; max: number; unlocked: boolean }> = { ...currentElements };
let floorHP = 0;
let floorMaxHP = 0;
const logMessages: string[] = [];
@@ -321,7 +323,7 @@ export function processGolemAttacks(
onDamageDealt,
applyDamageToRoom,
onApplyEnchantmentEffects,
});
}, rawMana, elements);
rawMana = basicResult.rawMana;
elements = basicResult.elements;
floorHP = basicResult.floorHP;
+7 -3
View File
@@ -95,9 +95,9 @@ export interface BasicAttackResult {
* AoE frames distribute damage across up to frame.aoeTargets enemies (spec §11).
* Single-target frames attack the lowest-HP enemy.
*/
export function processBasicAttack(ctx: BasicAttackContext): BasicAttackResult {
let rawMana = 0;
let elements: Record<string, { current: number; max: number; unlocked: boolean }> = {};
export function processBasicAttack(ctx: BasicAttackContext, currentRawMana: number, currentElements: Record<string, { current: number; max: number; unlocked: boolean }>): BasicAttackResult {
let rawMana = currentRawMana;
let elements: Record<string, { current: number; max: number; unlocked: boolean }> = { ...currentElements };
let floorHP = 0;
let floorMaxHP = 0;
let totalDamageDealt = 0;
@@ -174,6 +174,8 @@ export function processGolemAttacksFromStore(
enemyElement: string,
get: () => CombatStore,
set: (s: Partial<CombatState>) => void,
currentRawMana: number,
currentElements: Record<string, { current: number; max: number; unlocked: boolean }>,
): GolemCombatResult {
return processGolemAttacks(
activeGolems,
@@ -212,5 +214,7 @@ export function processGolemAttacksFromStore(
});
set({ currentRoom: { ...room, enemies: updatedEnemies } });
},
currentRawMana,
currentElements,
);
}