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
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s
This commit is contained in:
@@ -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 } });
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user