// ─── 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. * 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( 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 = frame.armorPierce + enchantmentBonusArmorPierce; dmg *= (1 + totalArmorPierce); 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; modifiedDamage?: number; }; applyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean }; onApplyEnchantmentEffects: (enemyId: string, effects: GolemEnchantmentEffect[]) => void; } export interface BasicAttackResult { rawMana: number; elements: Record; 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, currentRawMana: number, currentElements: Record): BasicAttackResult { let rawMana = currentRawMana; let elements: Record = { ...currentElements }; 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 ───────────────────────────────────────────── // 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, onDamageDealt: (damage: number) => { rawMana: number; elements: Record; modifiedDamage?: number; }, golemApplyDamageToRoom: (dmg: number) => { floorHP: number; floorMaxHP: number; roomCleared: boolean }, enemyElement: string, get: () => CombatStore, set: (s: Partial) => void, currentRawMana: number, currentElements: Record, ): 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)); }, () => { const room = get().currentRoom; return room.enemies.filter((e) => e.hp > 0); }, (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 } }); }, currentRawMana, currentElements, ); }