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:
@@ -45,26 +45,105 @@ export function resolveEnchantmentEffects(enchantmentIds: string[]): GolemEnchan
|
||||
|
||||
/**
|
||||
* 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(
|
||||
frame: { baseDamage: number; armorPierce: number; element?: string },
|
||||
enchantmentBonusArmorPierce: number,
|
||||
enemyArmor: 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);
|
||||
}
|
||||
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<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 ─────────────────────────────────────────────
|
||||
|
||||
// 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;
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user