fix: golem combat runtime - elemental matchup, enchantment effects, spell damage/cost, armor pierce (issue #313)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

This commit is contained in:
2026-06-08 10:12:18 +02:00
parent 0e1e506213
commit 1e99a57496
8 changed files with 691 additions and 73 deletions
+133
View File
@@ -0,0 +1,133 @@
// ─── Golem Combat Helpers ─────────────────────────────────────────────────────
// Shared helpers for golem combat: enchantment resolution, basic attack damage,
// and store-wrapper for processGolemAttacks.
// Extracted from golem-combat-actions.ts to stay under the 400-line file limit.
import { GOLEM_ENCHANTMENTS } from '../data/golems';
import { getElementalBonus } from '../utils';
import type { CombatStore, CombatState } from './combat-state.types';
import type {
ActiveEffect,
EnemyState,
RuntimeActiveGolem,
} from '../types';
// ─── Enchantment Effect Types ────────────────────────────────────────────────
export interface GolemEnchantmentEffect {
type: 'burn' | 'slow' | 'shock' | 'weaken' | 'armorPierce' | 'criticalChance' | 'soak' | 'shieldBreak';
magnitude: number;
}
// ─── Enchantment Resolution ──────────────────────────────────────────────────
/** Resolve enchantment effects from a list of enchantment IDs. */
export function resolveEnchantmentEffects(enchantmentIds: string[]): GolemEnchantmentEffect[] {
const effects: GolemEnchantmentEffect[] = [];
for (const id of enchantmentIds) {
const ench = GOLEM_ENCHANTMENTS[id];
if (!ench) continue;
switch (ench.effect) {
case 'burn': effects.push({ type: 'burn', magnitude: 3 }); break;
case 'slow': effects.push({ type: 'slow', magnitude: 0.3 }); break;
case 'shock': effects.push({ type: 'shock', magnitude: 0.25 }); break;
case 'weaken': effects.push({ type: 'weaken', magnitude: 0.2 }); break;
case 'armorPierce': effects.push({ type: 'armorPierce', magnitude: 0.15 }); break;
case 'criticalChance': effects.push({ type: 'criticalChance', magnitude: 0.1 }); break;
case 'soak': effects.push({ type: 'soak', magnitude: 0.3 }); break;
case 'shieldBreak': effects.push({ type: 'shieldBreak', magnitude: 0.25 }); break;
}
}
return effects;
}
// ─── Basic Attack Damage ─────────────────────────────────────────────────────
/**
* Compute basic attack damage for a golem.
* Applies elemental matchup bonus and proper armor pierce (bypasses armor fraction).
*/
export function computeBasicAttackDamage(
frame: { baseDamage: number; armorPierce: number; element?: string },
enchantmentBonusArmorPierce: number,
enemyArmor: number,
enemyElement: string,
): number {
let dmg = frame.baseDamage;
if (frame.element) {
dmg *= getElementalBonus(frame.element, enemyElement);
}
const totalArmorPierce = Math.min(1, frame.armorPierce + enchantmentBonusArmorPierce);
const effectiveArmor = enemyArmor * (1 - totalArmorPierce);
if (effectiveArmor > 0) {
dmg *= (1 - effectiveArmor);
}
return Math.max(0, dmg);
}
// ─── Golem Attacks Store Wrapper ─────────────────────────────────────────────
// Import here is safe: only used inside the function body, not at module init time.
import { processGolemAttacks } from './golem-combat-actions'; // eslint-disable-line
import type { GolemCombatResult } from './golem-combat-actions';
interface SerializedDesign {
id: string;
name: string;
coreId: string;
frameId: string;
mindCircuitId: string;
enchantmentIds: string[];
selectedManaTypes: string[];
selectedSpells: string[];
}
/** Convenience wrapper that wires up processGolemAttacks with store callbacks. */
export function processGolemAttacksFromStore(
activeGolems: RuntimeActiveGolem[],
golemDesigns: Record<string, SerializedDesign>,
onDamageDealt: (damage: number) => {
rawMana: number;
elements: Record<string, { current: number; max: number; unlocked: boolean }>;
modifiedDamage?: number;
},
golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean },
enemyElement: string,
get: () => CombatStore,
set: (s: Partial<CombatState>) => void,
): GolemCombatResult {
return processGolemAttacks(
activeGolems,
golemDesigns,
onDamageDealt,
golemApplyDamageToRoom,
enemyElement,
() => {
const room = get().currentRoom;
const living = room.enemies.filter((e) => e.hp > 0);
if (living.length === 0) return null;
return living.reduce((lowest, e) => (e.hp < lowest.hp ? e : lowest));
},
(enemyId, effects) => {
const room = get().currentRoom;
const updatedEnemies = room.enemies.map((e) => {
if (e.id !== enemyId) return e;
const newEffects = [...e.activeEffects];
for (const effect of effects) {
const idx = newEffects.findIndex((ae) => ae.type === effect.type);
if (idx >= 0) {
newEffects[idx] = {
...newEffects[idx],
remainingDuration: 4,
magnitude: Math.max(newEffects[idx].magnitude, effect.magnitude),
};
} else {
newEffects.push({ type: effect.type, remainingDuration: 4, magnitude: effect.magnitude, source: 'golem' });
}
}
return { ...e, activeEffects: newEffects };
});
set({ currentRoom: { ...room, enemies: updatedEnemies } });
},
);
}