fix(golemancy): reconcile spec vs code discrepancies (issue #326)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m21s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 2m21s
- D-SLOT-01: Verified slot cap of 7 matches spec §2.2 (no change needed) - D-COMB-03: Implement AoE damage distribution for Sand/Shadowglass frames - D-COMB-01: Reconcile armor pierce formula to spire-combat spec §9.4 (dmg × (1 + armorPierce)) - D-CIRC-01: Fix Simple Logic Circuit summon cost from raw to earth mana - D-ENCHANT-03: Add dual_attunement unlockRequirement to all golem enchantments - D-CORE-01/02: Add Guardian Core runtime override mechanism for guardian-specific mana Also increased test timeouts for module import tests that timeout in full suite runs.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-08T20:50:19.059Z
|
Generated: 2026-06-08T21:50:12.078Z
|
||||||
Found: 2 circular chain(s) — these MUST be fixed before modifying involved files.
|
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. 1) stores/golem-combat-actions.ts > stores/golem-combat-helpers.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-08T20:50:17.108Z",
|
"generated": "2026-06-08T21:50:10.004Z",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe('DebugTab module structure', () => {
|
|||||||
const mod = await import('./DebugTab');
|
const mod = await import('./DebugTab');
|
||||||
expect(mod.DebugTab).toBeDefined();
|
expect(mod.DebugTab).toBeDefined();
|
||||||
expect(typeof mod.DebugTab).toBe('function');
|
expect(typeof mod.DebugTab).toBe('function');
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
it('exports GameStateDebugSection', async () => {
|
it('exports GameStateDebugSection', async () => {
|
||||||
const mod = await import('./DebugTab/GameStateDebugSection');
|
const mod = await import('./DebugTab/GameStateDebugSection');
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('Tab barrel export', () => {
|
|||||||
const mod = await import('@/components/game/tabs');
|
const mod = await import('@/components/game/tabs');
|
||||||
expect(mod.GuardianPactsTab).toBeDefined();
|
expect(mod.GuardianPactsTab).toBeDefined();
|
||||||
expect(typeof mod.GuardianPactsTab).toBe('function');
|
expect(typeof mod.GuardianPactsTab).toBe('function');
|
||||||
});
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Test: Guardian data integrity ─────────────────────────────────────────────
|
// ─── Test: Guardian data integrity ─────────────────────────────────────────────
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ describe('PrestigeTab module structure', () => {
|
|||||||
const mod = await import('./PrestigeTab');
|
const mod = await import('./PrestigeTab');
|
||||||
expect(mod.PrestigeTab).toBeDefined();
|
expect(mod.PrestigeTab).toBeDefined();
|
||||||
expect(typeof mod.PrestigeTab).toBe('function');
|
expect(typeof mod.PrestigeTab).toBe('function');
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
it('PrestigeTab has correct displayName', async () => {
|
it('PrestigeTab has correct displayName', async () => {
|
||||||
const { PrestigeTab } = await import('./PrestigeTab');
|
const { PrestigeTab } = await import('./PrestigeTab');
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ describe('SpireSummaryTab module structure', () => {
|
|||||||
const mod = await import('./SpireSummaryTab');
|
const mod = await import('./SpireSummaryTab');
|
||||||
expect(mod.SpireSummaryTab).toBeDefined();
|
expect(mod.SpireSummaryTab).toBeDefined();
|
||||||
expect(typeof mod.SpireSummaryTab).toBe('function');
|
expect(typeof mod.SpireSummaryTab).toBe('function');
|
||||||
});
|
}, 30000);
|
||||||
|
|
||||||
it('SpireSummaryTab has correct displayName', async () => {
|
it('SpireSummaryTab has correct displayName', async () => {
|
||||||
const { SpireSummaryTab } = await import('./SpireSummaryTab');
|
const { SpireSummaryTab } = await import('./SpireSummaryTab');
|
||||||
|
|||||||
@@ -110,15 +110,15 @@ describe('computeGolemStats', () => {
|
|||||||
|
|
||||||
const stats = computeGolemStats(design);
|
const stats = computeGolemStats(design);
|
||||||
|
|
||||||
// basic core: 10 earth, earth frame: 5 raw, simple circuit: 3 raw
|
// basic core: 10 earth, earth frame: 5 raw, simple circuit: 3 earth (spec §6.1)
|
||||||
const rawCosts = stats.totalSummonCost.filter(c => c.type === 'raw');
|
const rawCosts = stats.totalSummonCost.filter(c => c.type === 'raw');
|
||||||
const earthCosts = stats.totalSummonCost.filter(c => c.type === 'element' && c.element === 'earth');
|
const earthCosts = stats.totalSummonCost.filter(c => c.type === 'element' && c.element === 'earth');
|
||||||
|
|
||||||
const totalRaw = rawCosts.reduce((sum, c) => sum + c.amount, 0);
|
const totalRaw = rawCosts.reduce((sum, c) => sum + c.amount, 0);
|
||||||
const totalEarth = earthCosts.reduce((sum, c) => sum + c.amount, 0);
|
const totalEarth = earthCosts.reduce((sum, c) => sum + c.amount, 0);
|
||||||
|
|
||||||
expect(totalRaw).toBe(8); // 5 + 3
|
expect(totalRaw).toBe(5); // earth frame only
|
||||||
expect(totalEarth).toBe(10);
|
expect(totalEarth).toBe(13); // basic core 10 + simple circuit 3
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ describe('canAffordGolemDesign', () => {
|
|||||||
selectedSpells: [],
|
selectedSpells: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// basic core: 10 earth, earth frame: 5 raw, simple circuit: 3 raw
|
// basic core: 10 earth, earth frame: 5 raw, simple circuit: 3 earth (spec §6.1)
|
||||||
const result = canAffordGolemDesign(design, 100, {
|
const result = canAffordGolemDesign(design, 100, {
|
||||||
earth: { current: 50, max: 100, unlocked: true },
|
earth: { current: 50, max: 100, unlocked: true },
|
||||||
});
|
});
|
||||||
@@ -164,7 +164,7 @@ describe('canAffordGolemDesign', () => {
|
|||||||
selectedSpells: [],
|
selectedSpells: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Need 8 raw total (5 + 3), only have 3
|
// Need 5 raw total (earth frame only, circuit uses earth mana now), only have 3
|
||||||
const result = canAffordGolemDesign(design, 3, {
|
const result = canAffordGolemDesign(design, 3, {
|
||||||
earth: { current: 50, max: 100, unlocked: true },
|
earth: { current: 50, max: 100, unlocked: true },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,12 @@
|
|||||||
import type { GolemEnchantmentDefinition } from './types';
|
import type { GolemEnchantmentDefinition } from './types';
|
||||||
import { elemCost } from './types';
|
import { elemCost } from './types';
|
||||||
|
|
||||||
|
const ENCHANTER_5_FABRICATOR_5 = {
|
||||||
|
type: 'dual_attunement' as const,
|
||||||
|
attunements: ['enchanter', 'fabricator'],
|
||||||
|
levels: [5, 5],
|
||||||
|
};
|
||||||
|
|
||||||
const SWORD_FIRE: GolemEnchantmentDefinition = {
|
const SWORD_FIRE: GolemEnchantmentDefinition = {
|
||||||
id: 'sword_fire',
|
id: 'sword_fire',
|
||||||
name: 'Sword: Fire',
|
name: 'Sword: Fire',
|
||||||
@@ -12,6 +18,7 @@ const SWORD_FIRE: GolemEnchantmentDefinition = {
|
|||||||
effect: 'burn',
|
effect: 'burn',
|
||||||
capacityCost: 10,
|
capacityCost: 10,
|
||||||
summonCost: [elemCost('fire', 5)],
|
summonCost: [elemCost('fire', 5)],
|
||||||
|
unlockRequirement: ENCHANTER_5_FABRICATOR_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SWORD_FROST: GolemEnchantmentDefinition = {
|
const SWORD_FROST: GolemEnchantmentDefinition = {
|
||||||
@@ -21,6 +28,7 @@ const SWORD_FROST: GolemEnchantmentDefinition = {
|
|||||||
effect: 'slow',
|
effect: 'slow',
|
||||||
capacityCost: 10,
|
capacityCost: 10,
|
||||||
summonCost: [elemCost('frost', 5)],
|
summonCost: [elemCost('frost', 5)],
|
||||||
|
unlockRequirement: ENCHANTER_5_FABRICATOR_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SWORD_LIGHTNING: GolemEnchantmentDefinition = {
|
const SWORD_LIGHTNING: GolemEnchantmentDefinition = {
|
||||||
@@ -30,6 +38,7 @@ const SWORD_LIGHTNING: GolemEnchantmentDefinition = {
|
|||||||
effect: 'shock',
|
effect: 'shock',
|
||||||
capacityCost: 12,
|
capacityCost: 12,
|
||||||
summonCost: [elemCost('lightning', 6)],
|
summonCost: [elemCost('lightning', 6)],
|
||||||
|
unlockRequirement: ENCHANTER_5_FABRICATOR_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SWORD_SHADOW: GolemEnchantmentDefinition = {
|
const SWORD_SHADOW: GolemEnchantmentDefinition = {
|
||||||
@@ -39,6 +48,7 @@ const SWORD_SHADOW: GolemEnchantmentDefinition = {
|
|||||||
effect: 'weaken',
|
effect: 'weaken',
|
||||||
capacityCost: 12,
|
capacityCost: 12,
|
||||||
summonCost: [elemCost('dark', 6)],
|
summonCost: [elemCost('dark', 6)],
|
||||||
|
unlockRequirement: ENCHANTER_5_FABRICATOR_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SWORD_METAL: GolemEnchantmentDefinition = {
|
const SWORD_METAL: GolemEnchantmentDefinition = {
|
||||||
@@ -48,6 +58,7 @@ const SWORD_METAL: GolemEnchantmentDefinition = {
|
|||||||
effect: 'armorPierce',
|
effect: 'armorPierce',
|
||||||
capacityCost: 8,
|
capacityCost: 8,
|
||||||
summonCost: [elemCost('metal', 5)],
|
summonCost: [elemCost('metal', 5)],
|
||||||
|
unlockRequirement: ENCHANTER_5_FABRICATOR_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SWORD_CRYSTAL: GolemEnchantmentDefinition = {
|
const SWORD_CRYSTAL: GolemEnchantmentDefinition = {
|
||||||
@@ -57,6 +68,7 @@ const SWORD_CRYSTAL: GolemEnchantmentDefinition = {
|
|||||||
effect: 'criticalChance',
|
effect: 'criticalChance',
|
||||||
capacityCost: 14,
|
capacityCost: 14,
|
||||||
summonCost: [elemCost('crystal', 7)],
|
summonCost: [elemCost('crystal', 7)],
|
||||||
|
unlockRequirement: ENCHANTER_5_FABRICATOR_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SWORD_WATER: GolemEnchantmentDefinition = {
|
const SWORD_WATER: GolemEnchantmentDefinition = {
|
||||||
@@ -66,6 +78,7 @@ const SWORD_WATER: GolemEnchantmentDefinition = {
|
|||||||
effect: 'soak',
|
effect: 'soak',
|
||||||
capacityCost: 8,
|
capacityCost: 8,
|
||||||
summonCost: [elemCost('water', 4)],
|
summonCost: [elemCost('water', 4)],
|
||||||
|
unlockRequirement: ENCHANTER_5_FABRICATOR_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SWORD_EARTH: GolemEnchantmentDefinition = {
|
const SWORD_EARTH: GolemEnchantmentDefinition = {
|
||||||
@@ -75,6 +88,7 @@ const SWORD_EARTH: GolemEnchantmentDefinition = {
|
|||||||
effect: 'shieldBreak',
|
effect: 'shieldBreak',
|
||||||
capacityCost: 10,
|
capacityCost: 10,
|
||||||
summonCost: [elemCost('earth', 5)],
|
summonCost: [elemCost('earth', 5)],
|
||||||
|
unlockRequirement: ENCHANTER_5_FABRICATOR_5,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── ENCHANTMENT REGISTRY ────────────────────────────────────────────────
|
// ─── ENCHANTMENT REGISTRY ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type {
|
|||||||
CircuitBehavior,
|
CircuitBehavior,
|
||||||
GolemEnchantmentDefinition,
|
GolemEnchantmentDefinition,
|
||||||
GolemDesign,
|
GolemDesign,
|
||||||
|
SerializedDesign,
|
||||||
ComputedGolemStats,
|
ComputedGolemStats,
|
||||||
GolemManaCost,
|
GolemManaCost,
|
||||||
GolemUnlockRequirement,
|
GolemUnlockRequirement,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const SIMPLE_CIRCUIT: MindCircuitDefinition = {
|
|||||||
description: 'Performs basic attacks only. Targets nearest enemy. No spell casting.',
|
description: 'Performs basic attacks only. Targets nearest enemy. No spell casting.',
|
||||||
spellSlots: 0,
|
spellSlots: 0,
|
||||||
behavior: 'basicOnly',
|
behavior: 'basicOnly',
|
||||||
summonCost: [rawCost(3)],
|
summonCost: [elemCost('earth', 3)],
|
||||||
unlockRequirement: {
|
unlockRequirement: {
|
||||||
type: 'attunement_level',
|
type: 'attunement_level',
|
||||||
attunement: 'fabricator',
|
attunement: 'fabricator',
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export interface GolemEnchantmentDefinition {
|
|||||||
effect: string;
|
effect: string;
|
||||||
capacityCost: number;
|
capacityCost: number;
|
||||||
summonCost: GolemManaCost[];
|
summonCost: GolemManaCost[];
|
||||||
|
unlockRequirement: GolemUnlockRequirement;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Golem Design (Player-Created) ──────────────────────────────────────
|
// ─── Golem Design (Player-Created) ──────────────────────────────────────
|
||||||
@@ -114,6 +115,33 @@ export interface GolemDesign {
|
|||||||
selectedManaTypes: string[];
|
selectedManaTypes: string[];
|
||||||
/** Player-selected spell IDs for mind circuits with spell slots */
|
/** Player-selected spell IDs for mind circuits with spell slots */
|
||||||
selectedSpells: string[];
|
selectedSpells: string[];
|
||||||
|
/**
|
||||||
|
* Runtime override for Guardian Core summon cost.
|
||||||
|
* When core is 'guardian', this replaces the placeholder earth cost
|
||||||
|
* with guardian-specific mana costs derived from the signed pact.
|
||||||
|
* Spec §4.3: Guardian Core uses guardian-specific mana types.
|
||||||
|
*/
|
||||||
|
guardianSummonCost?: GolemManaCost[];
|
||||||
|
/**
|
||||||
|
* Runtime override for Guardian Core primary mana type.
|
||||||
|
* Spec §4.3: Provides all mana types granted by the chosen Guardian.
|
||||||
|
*/
|
||||||
|
guardianPrimaryManaType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Serialized Design (for storage/transmission) ──────────────────────
|
||||||
|
|
||||||
|
export interface SerializedDesign {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
coreId: string;
|
||||||
|
frameId: string;
|
||||||
|
mindCircuitId: string;
|
||||||
|
enchantmentIds: string[];
|
||||||
|
selectedManaTypes: string[];
|
||||||
|
selectedSpells: string[];
|
||||||
|
guardianSummonCost?: GolemManaCost[];
|
||||||
|
guardianPrimaryManaType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Computed Design Stats (derived from components) ────────────────────
|
// ─── Computed Design Stats (derived from components) ────────────────────
|
||||||
|
|||||||
@@ -74,17 +74,27 @@ export function computeGolemStats(design: GolemDesign): ComputedGolemStats {
|
|||||||
const enchantments = design.enchantments;
|
const enchantments = design.enchantments;
|
||||||
|
|
||||||
// Total summon cost from all components
|
// Total summon cost from all components
|
||||||
|
// For Guardian Core, use guardian-specific summon cost override if provided (spec §4.3)
|
||||||
|
const coreSummonCost = core.id === 'guardian' && design.guardianSummonCost
|
||||||
|
? design.guardianSummonCost
|
||||||
|
: core.summonCost;
|
||||||
const totalSummonCost: GolemManaCost[] = [
|
const totalSummonCost: GolemManaCost[] = [
|
||||||
...core.summonCost,
|
...coreSummonCost,
|
||||||
...frame.summonCost,
|
...frame.summonCost,
|
||||||
...circuit.summonCost,
|
...circuit.summonCost,
|
||||||
...enchantments.flatMap((e) => e.summonCost),
|
...enchantments.flatMap((e) => e.summonCost),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Player upkeep = Core.manaRegen × 2 per hour, split across all mana types (spec §13)
|
// Player upkeep = Core.manaRegen × 2 per hour, split across all mana types (spec §13)
|
||||||
|
// For Guardian Core, use guardian-specific primary mana type if provided (spec §4.3)
|
||||||
|
const effectivePrimaryManaType = core.id === 'guardian' && design.guardianPrimaryManaType
|
||||||
|
? design.guardianPrimaryManaType
|
||||||
|
: core.primaryManaType;
|
||||||
const upkeepManaTypes = design.selectedManaTypes.length > 0
|
const upkeepManaTypes = design.selectedManaTypes.length > 0
|
||||||
? design.selectedManaTypes
|
? design.selectedManaTypes
|
||||||
: core.manaTypes;
|
: (core.id === 'guardian' && design.guardianPrimaryManaType
|
||||||
|
? [design.guardianPrimaryManaType]
|
||||||
|
: core.manaTypes);
|
||||||
const upkeepPerType = (core.manaRegen * 2) / Math.max(1, upkeepManaTypes.length);
|
const upkeepPerType = (core.manaRegen * 2) / Math.max(1, upkeepManaTypes.length);
|
||||||
const upkeepCostPerHour: GolemManaCost[] = upkeepManaTypes.map((mt) => ({
|
const upkeepCostPerHour: GolemManaCost[] = upkeepManaTypes.map((mt) => ({
|
||||||
type: 'element' as const,
|
type: 'element' as const,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ function runGolemAttacks(
|
|||||||
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
||||||
enemyElement,
|
enemyElement,
|
||||||
() => enemy,
|
() => enemy,
|
||||||
|
() => [enemy],
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
return { result, capturedDamage };
|
return { result, capturedDamage };
|
||||||
@@ -133,6 +134,7 @@ describe('processGolemAttacks - spell damage and mana cost (fixes #1, #2)', () =
|
|||||||
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
||||||
'fire',
|
'fire',
|
||||||
() => enemy,
|
() => enemy,
|
||||||
|
() => [enemy],
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -156,6 +158,7 @@ describe('processGolemAttacks - spell damage and mana cost (fixes #1, #2)', () =
|
|||||||
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
||||||
'fire',
|
'fire',
|
||||||
() => enemy,
|
() => enemy,
|
||||||
|
() => [enemy],
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -214,10 +217,12 @@ describe('processGolemAttacks - elemental matchup (fix #3)', () => {
|
|||||||
const enemy = makeEnemy({ element: 'fire', armor: 0 }); // No armor for clean test
|
const enemy = makeEnemy({ element: 'fire', armor: 0 }); // No armor for clean test
|
||||||
|
|
||||||
const frame = FRAMES['crystal'];
|
const frame = FRAMES['crystal'];
|
||||||
const expectedDmg = frame.baseDamage; // 1.0x neutral, no armor
|
// Crystal frame: baseDamage=14, armorPierce=0.15, neutral element (1.0x)
|
||||||
|
// New formula: 14 * 1.0 * (1 + 0.15) = 16.1
|
||||||
|
const expectedDmg = frame.baseDamage * (1 + frame.armorPierce);
|
||||||
|
|
||||||
const { capturedDamage } = runGolemAttacks(golem, serialized, design, enemy, 'fire');
|
const { capturedDamage } = runGolemAttacks(golem, serialized, design, enemy, 'fire');
|
||||||
expect(capturedDamage).toBe(expectedDmg);
|
expect(capturedDamage).toBeCloseTo(expectedDmg, 5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,6 +243,7 @@ describe('processGolemAttacks - enchantment effects (fix #4)', () => {
|
|||||||
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
||||||
'fire',
|
'fire',
|
||||||
() => enemy,
|
() => enemy,
|
||||||
|
() => [enemy],
|
||||||
(enemyId, effects) => { appliedEffects.push({ enemyId, effects }); },
|
(enemyId, effects) => { appliedEffects.push({ enemyId, effects }); },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -259,6 +265,7 @@ describe('processGolemAttacks - enchantment effects (fix #4)', () => {
|
|||||||
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
||||||
'fire',
|
'fire',
|
||||||
() => enemy,
|
() => enemy,
|
||||||
|
() => [enemy],
|
||||||
(_, effects) => { appliedEffects.push(effects); },
|
(_, effects) => { appliedEffects.push(effects); },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -282,6 +289,7 @@ describe('processGolemAttacks - enchantment effects (fix #4)', () => {
|
|||||||
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
() => ({ floorHP: 50, floorMaxHP: 100, roomCleared: false }),
|
||||||
'fire',
|
'fire',
|
||||||
() => enemy,
|
() => enemy,
|
||||||
|
() => [enemy],
|
||||||
() => { effectsCalled = true; },
|
() => { effectsCalled = true; },
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -289,58 +297,55 @@ describe('processGolemAttacks - enchantment effects (fix #4)', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Fix #5: Armor pierce ────────────────────────────────────────────────────
|
// ─── Fix #5: Armor pierce (spire-combat spec §9.4) ──────────────────────────
|
||||||
|
// Formula: dmg = frame.baseDamage × (1 + frame.armorPierce)
|
||||||
|
// Enemy armor reduction is handled separately in onDamageDealt.
|
||||||
|
|
||||||
describe('processGolemAttacks - armor pierce (fix #5)', () => {
|
describe('processGolemAttacks - armor pierce (fix #5)', () => {
|
||||||
it('does not multiply damage by (1 + armorPierce)', () => {
|
it('applies armor pierce as damage multiplier per spire-combat spec §9.4', () => {
|
||||||
// Steel frame: baseDamage=18, armorPierce=0.5
|
// Steel frame: baseDamage=18, armorPierce=0.5
|
||||||
// Old buggy formula: 18 * (1 + 0.5) = 27
|
// New formula: 18 * (1 + 0.5) = 27
|
||||||
// Correct: 18 with 50% armor bypass against 40% armor
|
|
||||||
const design = makeDesign('basic', 'steel', 'simple');
|
const design = makeDesign('basic', 'steel', 'simple');
|
||||||
const serialized = makeSerialized(design);
|
const serialized = makeSerialized(design);
|
||||||
const golem = makeActiveGolem(design, undefined, 1.0);
|
const golem = makeActiveGolem(design, undefined, 1.0);
|
||||||
const enemy = makeEnemy({ armor: 0.4 });
|
const enemy = makeEnemy({ armor: 0.4 });
|
||||||
|
|
||||||
const frame = FRAMES['steel'];
|
const frame = FRAMES['steel'];
|
||||||
// With 50% armor pierce: effectiveArmor = 0.4 * (1 - 0.5) = 0.2
|
const expectedDmg = frame.baseDamage * (1 + frame.armorPierce);
|
||||||
// dmg = 18 * (1 - 0.2) = 14.4
|
|
||||||
const expectedDmg = frame.baseDamage * (1 - 0.4 * (1 - frame.armorPierce));
|
|
||||||
|
|
||||||
const { capturedDamage } = runGolemAttacks(golem, serialized, design, enemy, 'fire');
|
const { capturedDamage } = runGolemAttacks(golem, serialized, design, enemy, 'fire');
|
||||||
|
|
||||||
// Should NOT be 18 * 1.5 = 27 (old buggy formula)
|
|
||||||
expect(capturedDamage).not.toBe(frame.baseDamage * (1 + frame.armorPierce));
|
|
||||||
// Should be the correct armor-bypassed value
|
|
||||||
expect(capturedDamage).toBe(expectedDmg);
|
expect(capturedDamage).toBe(expectedDmg);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fully bypasses armor when armorPierce is 1.0', () => {
|
it('doubles damage when armorPierce is 1.0', () => {
|
||||||
|
// dmg = 10 * (1 + 1.0) = 20
|
||||||
const dmg = computeBasicAttackDamage(
|
const dmg = computeBasicAttackDamage(
|
||||||
{ baseDamage: 10, armorPierce: 1.0, element: undefined },
|
{ baseDamage: 10, armorPierce: 1.0, element: undefined },
|
||||||
0, 0.8, 'fire',
|
0, 0.8, 'fire',
|
||||||
);
|
);
|
||||||
expect(dmg).toBe(10); // Full damage, armor fully bypassed
|
expect(dmg).toBe(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies no armor bypass when armorPierce is 0', () => {
|
it('returns base damage when armorPierce is 0', () => {
|
||||||
|
// dmg = 10 * (1 + 0) = 10
|
||||||
const dmg = computeBasicAttackDamage(
|
const dmg = computeBasicAttackDamage(
|
||||||
{ baseDamage: 10, armorPierce: 0, element: undefined },
|
{ baseDamage: 10, armorPierce: 0, element: undefined },
|
||||||
0, 0.5, 'fire',
|
0, 0.5, 'fire',
|
||||||
);
|
);
|
||||||
// 50% armor, no pierce: 10 * (1 - 0.5) = 5
|
expect(dmg).toBe(10);
|
||||||
expect(dmg).toBe(5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stacks enchantment armorPierce with frame armorPierce', () => {
|
it('stacks enchantment armorPierce with frame armorPierce', () => {
|
||||||
const framePierce = 0.5;
|
const framePierce = 0.5;
|
||||||
const enchantPierce = 0.15;
|
const enchantPierce = 0.15;
|
||||||
const totalPierce = Math.min(1, framePierce + enchantPierce);
|
const totalPierce = framePierce + enchantPierce;
|
||||||
|
|
||||||
const dmg = computeBasicAttackDamage(
|
const dmg = computeBasicAttackDamage(
|
||||||
{ baseDamage: 20, armorPierce: framePierce, element: undefined },
|
{ baseDamage: 20, armorPierce: framePierce, element: undefined },
|
||||||
enchantPierce, 0.4, 'fire',
|
enchantPierce, 0.4, 'fire',
|
||||||
);
|
);
|
||||||
const expected = 20 * (1 - 0.4 * (1 - totalPierce));
|
const expected = 20 * (1 + totalPierce);
|
||||||
expect(dmg).toBe(expected);
|
expect(dmg).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import { computeGolemStats, getGolemSlots } from '../data/golems/utils';
|
|||||||
import {
|
import {
|
||||||
resolveEnchantmentEffects,
|
resolveEnchantmentEffects,
|
||||||
computeBasicAttackDamage,
|
computeBasicAttackDamage,
|
||||||
|
processBasicAttack,
|
||||||
} from './golem-combat-helpers';
|
} from './golem-combat-helpers';
|
||||||
|
import type { BasicAttackResult } from './golem-combat-helpers';
|
||||||
import type { GolemEnchantmentEffect } from './golem-combat-helpers';
|
import type { GolemEnchantmentEffect } from './golem-combat-helpers';
|
||||||
|
import type { SerializedDesign } from '../data/golems/types';
|
||||||
import type {
|
import type {
|
||||||
RuntimeActiveGolem,
|
RuntimeActiveGolem,
|
||||||
GolemLoadoutEntry,
|
GolemLoadoutEntry,
|
||||||
@@ -27,17 +30,6 @@ export interface GolemCombatResult {
|
|||||||
totalDamageDealt: number;
|
totalDamageDealt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SerializedDesign {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
coreId: string;
|
|
||||||
frameId: string;
|
|
||||||
mindCircuitId: string;
|
|
||||||
enchantmentIds: string[];
|
|
||||||
selectedManaTypes: string[];
|
|
||||||
selectedSpells: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Summoning (spec §10) ───────────────────────────────────────────────────
|
// ─── Summoning (spec §10) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,6 +87,8 @@ export function summonGolemsOnRoomEntry(
|
|||||||
enchantments: [],
|
enchantments: [],
|
||||||
selectedManaTypes: design.selectedManaTypes,
|
selectedManaTypes: design.selectedManaTypes,
|
||||||
selectedSpells: design.selectedSpells,
|
selectedSpells: design.selectedSpells,
|
||||||
|
guardianSummonCost: design.guardianSummonCost,
|
||||||
|
guardianPrimaryManaType: design.guardianPrimaryManaType,
|
||||||
});
|
});
|
||||||
|
|
||||||
let canAfford = true;
|
let canAfford = true;
|
||||||
@@ -239,7 +233,7 @@ export function processGolemManaRegen(
|
|||||||
* 2. Spell mana cost: uses actual SPELLS_DEF[spellId].cost.amount
|
* 2. Spell mana cost: uses actual SPELLS_DEF[spellId].cost.amount
|
||||||
* 3. Elemental matchup: applies getElementalBonus to basic attacks
|
* 3. Elemental matchup: applies getElementalBonus to basic attacks
|
||||||
* 4. Enchantment effects: applies golem enchantment effects on basic attacks
|
* 4. Enchantment effects: applies golem enchantment effects on basic attacks
|
||||||
* 5. Armor pierce: bypasses enemy armor fraction instead of multiplying damage
|
* 5. Armor pierce: per spire-combat spec §9.4, dmg = baseDamage × (1 + armorPierce)
|
||||||
*/
|
*/
|
||||||
export function processGolemAttacks(
|
export function processGolemAttacks(
|
||||||
activeGolems: RuntimeActiveGolem[],
|
activeGolems: RuntimeActiveGolem[],
|
||||||
@@ -252,6 +246,7 @@ export function processGolemAttacks(
|
|||||||
applyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
|
applyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
|
||||||
enemyElement: string,
|
enemyElement: string,
|
||||||
getTargetEnemy: () => EnemyState | null,
|
getTargetEnemy: () => EnemyState | null,
|
||||||
|
getTargetEnemies: () => EnemyState[],
|
||||||
onApplyEnchantmentEffects: (enemyId: string, effects: GolemEnchantmentEffect[]) => void,
|
onApplyEnchantmentEffects: (enemyId: string, effects: GolemEnchantmentEffect[]) => void,
|
||||||
): GolemCombatResult {
|
): GolemCombatResult {
|
||||||
let rawMana = 0;
|
let rawMana = 0;
|
||||||
@@ -273,6 +268,7 @@ export function processGolemAttacks(
|
|||||||
|
|
||||||
const enchantmentEffects = resolveEnchantmentEffects(design.enchantmentIds);
|
const enchantmentEffects = resolveEnchantmentEffects(design.enchantmentIds);
|
||||||
const bonusArmorPierce = design.enchantmentIds.includes('sword_metal') ? 0.15 : 0;
|
const bonusArmorPierce = design.enchantmentIds.includes('sword_metal') ? 0.15 : 0;
|
||||||
|
const isAoe = frame.aoeTargets > 1;
|
||||||
|
|
||||||
let attackProgress = golem.attackProgress + HOURS_PER_TICK * frame.attackSpeed;
|
let attackProgress = golem.attackProgress + HOURS_PER_TICK * frame.attackSpeed;
|
||||||
const updatedGolem = { ...golem };
|
const updatedGolem = { ...golem };
|
||||||
@@ -314,29 +310,23 @@ export function processGolemAttacks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic attack
|
// Basic attack: AoE or single-target (delegated to helper)
|
||||||
// FIX #3, #4, #5: elemental matchup, enchantment effects, proper armor pierce
|
const basicResult: BasicAttackResult = processBasicAttack({
|
||||||
const targetEnemy = getTargetEnemy();
|
frame: { ...frame, aoeTargets: frame.aoeTargets },
|
||||||
const enemyArmor = targetEnemy?.armor ?? 0;
|
bonusArmorPierce,
|
||||||
|
enchantmentEffects,
|
||||||
const dmg = computeBasicAttackDamage(frame, bonusArmorPierce, enemyArmor, enemyElement);
|
enemyElement,
|
||||||
|
getTargetEnemy,
|
||||||
// Apply enchantment effects to target enemy
|
getTargetEnemies,
|
||||||
if (enchantmentEffects.length > 0 && targetEnemy) {
|
onDamageDealt,
|
||||||
onApplyEnchantmentEffects(targetEnemy.id, enchantmentEffects);
|
applyDamageToRoom,
|
||||||
}
|
onApplyEnchantmentEffects,
|
||||||
|
});
|
||||||
const dmgResult = onDamageDealt(dmg, true);
|
rawMana = basicResult.rawMana;
|
||||||
const finalDamage = dmgResult.modifiedDamage || dmg;
|
elements = basicResult.elements;
|
||||||
|
floorHP = basicResult.floorHP;
|
||||||
if (Number.isFinite(finalDamage)) {
|
floorMaxHP = basicResult.floorMaxHP;
|
||||||
const roomResult = applyDamageToRoom(finalDamage);
|
totalDamageDealt += basicResult.totalDamageDealt;
|
||||||
floorHP = roomResult.floorHP;
|
|
||||||
floorMaxHP = roomResult.floorMaxHP;
|
|
||||||
totalDamageDealt += Math.max(0, finalDamage);
|
|
||||||
rawMana = dmgResult.rawMana;
|
|
||||||
elements = dmgResult.elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
attackProgress -= 1;
|
attackProgress -= 1;
|
||||||
safetyCounter++;
|
safetyCounter++;
|
||||||
|
|||||||
@@ -50,39 +50,42 @@ describe('computeBasicAttackDamage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('combines elemental bonus and armor pierce', () => {
|
it('combines elemental bonus and armor pierce', () => {
|
||||||
// Water vs Fire = 1.5x, then 50% armor with 25% pierce
|
// Water vs Fire = 1.5x, then armor pierce adds to damage multiplier
|
||||||
// effectiveArmor = 0.5 * (1 - 0.25) = 0.375
|
// dmg = 10 * 1.5 * (1 + 0.25) = 18.75
|
||||||
// dmg = 10 * 1.5 * (1 - 0.375) = 9.375
|
|
||||||
const dmg = computeBasicAttackDamage(
|
const dmg = computeBasicAttackDamage(
|
||||||
{ baseDamage: 10, armorPierce: 0.25, element: 'water' },
|
{ baseDamage: 10, armorPierce: 0.25, element: 'water' },
|
||||||
0, 0.5, 'fire',
|
0, 0.5, 'fire',
|
||||||
);
|
);
|
||||||
expect(dmg).toBeCloseTo(9.375, 5);
|
expect(dmg).toBeCloseTo(18.75, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fully bypasses armor when armorPierce is 1.0', () => {
|
it('doubles damage when armorPierce is 1.0', () => {
|
||||||
|
// dmg = 10 * (1 + 1.0) = 20
|
||||||
const dmg = computeBasicAttackDamage(
|
const dmg = computeBasicAttackDamage(
|
||||||
{ baseDamage: 10, armorPierce: 1.0, element: undefined },
|
{ baseDamage: 10, armorPierce: 1.0, element: undefined },
|
||||||
0, 0.8, 'fire',
|
0, 0.8, 'fire',
|
||||||
);
|
);
|
||||||
expect(dmg).toBe(10);
|
expect(dmg).toBe(20);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies no armor bypass when armorPierce is 0', () => {
|
it('returns base damage when armorPierce is 0', () => {
|
||||||
|
// dmg = 10 * (1 + 0) = 10
|
||||||
const dmg = computeBasicAttackDamage(
|
const dmg = computeBasicAttackDamage(
|
||||||
{ baseDamage: 10, armorPierce: 0, element: undefined },
|
{ baseDamage: 10, armorPierce: 0, element: undefined },
|
||||||
0, 0.5, 'fire',
|
0, 0.5, 'fire',
|
||||||
);
|
);
|
||||||
expect(dmg).toBe(5);
|
expect(dmg).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stacks enchantment armorPierce with frame armorPierce', () => {
|
it('stacks enchantment armorPierce with frame armorPierce', () => {
|
||||||
const totalPierce = Math.min(1, 0.5 + 0.15);
|
// totalPierce = 0.5 + 0.15 = 0.65
|
||||||
|
// dmg = 20 * (1 + 0.65) = 33
|
||||||
|
const totalPierce = 0.5 + 0.15;
|
||||||
const dmg = computeBasicAttackDamage(
|
const dmg = computeBasicAttackDamage(
|
||||||
{ baseDamage: 20, armorPierce: 0.5, element: undefined },
|
{ baseDamage: 20, armorPierce: 0.5, element: undefined },
|
||||||
0.15, 0.4, 'fire',
|
0.15, 0.4, 'fire',
|
||||||
);
|
);
|
||||||
expect(dmg).toBe(20 * (1 - 0.4 * (1 - totalPierce)));
|
expect(dmg).toBe(20 * (1 + totalPierce));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,26 +45,105 @@ export function resolveEnchantmentEffects(enchantmentIds: string[]): GolemEnchan
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute basic attack damage for a golem.
|
* Compute basic attack damage for a golem.
|
||||||
* Applies elemental matchup bonus and proper armor pierce (bypasses armor fraction).
|
* Formula per spire-combat spec §9.4: dmg = frame.baseDamage × (1 + frame.armorPierce)
|
||||||
|
* Also applies elemental matchup bonus and enchantment armor pierce bonus.
|
||||||
|
* Enemy armor reduction is handled separately in onDamageDealt.
|
||||||
*/
|
*/
|
||||||
export function computeBasicAttackDamage(
|
export function computeBasicAttackDamage(
|
||||||
frame: { baseDamage: number; armorPierce: number; element?: string },
|
frame: { baseDamage: number; armorPierce: number; element?: string },
|
||||||
enchantmentBonusArmorPierce: number,
|
enchantmentBonusArmorPierce: number,
|
||||||
enemyArmor: number,
|
_enemyArmor: number,
|
||||||
enemyElement: string,
|
enemyElement: string,
|
||||||
): number {
|
): number {
|
||||||
let dmg = frame.baseDamage;
|
let dmg = frame.baseDamage;
|
||||||
if (frame.element) {
|
if (frame.element) {
|
||||||
dmg *= getElementalBonus(frame.element, enemyElement);
|
dmg *= getElementalBonus(frame.element, enemyElement);
|
||||||
}
|
}
|
||||||
const totalArmorPierce = Math.min(1, frame.armorPierce + enchantmentBonusArmorPierce);
|
const totalArmorPierce = frame.armorPierce + enchantmentBonusArmorPierce;
|
||||||
const effectiveArmor = enemyArmor * (1 - totalArmorPierce);
|
dmg *= (1 + totalArmorPierce);
|
||||||
if (effectiveArmor > 0) {
|
|
||||||
dmg *= (1 - effectiveArmor);
|
|
||||||
}
|
|
||||||
return Math.max(0, dmg);
|
return Math.max(0, dmg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Basic Attack Processing ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface BasicAttackContext {
|
||||||
|
frame: { baseDamage: number; armorPierce: number; element?: string; aoeTargets: number };
|
||||||
|
bonusArmorPierce: number;
|
||||||
|
enchantmentEffects: GolemEnchantmentEffect[];
|
||||||
|
enemyElement: string;
|
||||||
|
getTargetEnemy: () => EnemyState | null;
|
||||||
|
getTargetEnemies: () => EnemyState[];
|
||||||
|
onDamageDealt: (damage: number, skipSpecials?: boolean) => {
|
||||||
|
rawMana: number;
|
||||||
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||||
|
modifiedDamage?: number;
|
||||||
|
};
|
||||||
|
applyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean };
|
||||||
|
onApplyEnchantmentEffects: (enemyId: string, effects: GolemEnchantmentEffect[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BasicAttackResult {
|
||||||
|
rawMana: number;
|
||||||
|
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
|
||||||
|
floorHP: number;
|
||||||
|
floorMaxHP: number;
|
||||||
|
totalDamageDealt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single basic attack (AoE or single-target) for a golem.
|
||||||
|
* 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 }> = {};
|
||||||
|
let floorHP = 0;
|
||||||
|
let floorMaxHP = 0;
|
||||||
|
let totalDamageDealt = 0;
|
||||||
|
|
||||||
|
if (ctx.frame.aoeTargets > 1) {
|
||||||
|
const allEnemies = ctx.getTargetEnemies();
|
||||||
|
if (allEnemies.length > 0) {
|
||||||
|
const targets = allEnemies.slice(0, ctx.frame.aoeTargets);
|
||||||
|
const dmgPerTarget = computeBasicAttackDamage(ctx.frame, ctx.bonusArmorPierce, 0, ctx.enemyElement) / targets.length;
|
||||||
|
for (const target of targets) {
|
||||||
|
if (ctx.enchantmentEffects.length > 0) {
|
||||||
|
ctx.onApplyEnchantmentEffects(target.id, ctx.enchantmentEffects);
|
||||||
|
}
|
||||||
|
const dmgResult = ctx.onDamageDealt(dmgPerTarget, true);
|
||||||
|
const finalDamage = dmgResult.modifiedDamage || dmgPerTarget;
|
||||||
|
if (Number.isFinite(finalDamage)) {
|
||||||
|
const roomResult = ctx.applyDamageToRoom(finalDamage);
|
||||||
|
floorHP = roomResult.floorHP;
|
||||||
|
floorMaxHP = roomResult.floorMaxHP;
|
||||||
|
totalDamageDealt += Math.max(0, finalDamage);
|
||||||
|
rawMana = dmgResult.rawMana;
|
||||||
|
elements = dmgResult.elements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const targetEnemy = ctx.getTargetEnemy();
|
||||||
|
const dmg = computeBasicAttackDamage(ctx.frame, ctx.bonusArmorPierce, 0, ctx.enemyElement);
|
||||||
|
if (ctx.enchantmentEffects.length > 0 && targetEnemy) {
|
||||||
|
ctx.onApplyEnchantmentEffects(targetEnemy.id, ctx.enchantmentEffects);
|
||||||
|
}
|
||||||
|
const dmgResult = ctx.onDamageDealt(dmg, true);
|
||||||
|
const finalDamage = dmgResult.modifiedDamage || dmg;
|
||||||
|
if (Number.isFinite(finalDamage)) {
|
||||||
|
const roomResult = ctx.applyDamageToRoom(finalDamage);
|
||||||
|
floorHP = roomResult.floorHP;
|
||||||
|
floorMaxHP = roomResult.floorMaxHP;
|
||||||
|
totalDamageDealt += Math.max(0, finalDamage);
|
||||||
|
rawMana = dmgResult.rawMana;
|
||||||
|
elements = dmgResult.elements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rawMana, elements, floorHP, floorMaxHP, totalDamageDealt };
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Golem Attacks Store Wrapper ─────────────────────────────────────────────
|
// ─── Golem Attacks Store Wrapper ─────────────────────────────────────────────
|
||||||
|
|
||||||
// Import here is safe: only used inside the function body, not at module init time.
|
// Import here is safe: only used inside the function body, not at module init time.
|
||||||
@@ -108,6 +187,10 @@ export function processGolemAttacksFromStore(
|
|||||||
if (living.length === 0) return null;
|
if (living.length === 0) return null;
|
||||||
return living.reduce((lowest, e) => (e.hp < lowest.hp ? e : lowest));
|
return living.reduce((lowest, e) => (e.hp < lowest.hp ? e : lowest));
|
||||||
},
|
},
|
||||||
|
() => {
|
||||||
|
const room = get().currentRoom;
|
||||||
|
return room.enemies.filter((e) => e.hp > 0);
|
||||||
|
},
|
||||||
(enemyId, effects) => {
|
(enemyId, effects) => {
|
||||||
const room = get().currentRoom;
|
const room = get().currentRoom;
|
||||||
const updatedEnemies = room.enemies.map((e) => {
|
const updatedEnemies = room.enemies.map((e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user