fix: spire combat 11 high-severity discrepancies (issue #333)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m19s
D-01: Implement per-weapon cast progress (weaponCastProgress record) D-04: Bypass Executioner/Berserker discipline specials for golem attacks D-09: Fix lightning counter direction (lightning→water, not lightning→earth) D-10: Add full composite element counters (blackflame/radiantflames ↔ frost/water/light/dark) D-15: Fix Executioner to check per-enemy HP < 25% instead of floorHP ratio D-20: Fix dodge formula to match spec (min(0.55, floor × 0.003), starts at 0) D-22: Fix shield modifier to use flat HP pool instead of percentage barrier D-23: Wire up applyMageBarrierRecharge in the damage pipeline D-25: Move guardian regen from per-damage-event to once-per-tick D-26: Add guardian armor reduction to the guardian defensive pipeline D-31: Fix armor_corrode to be temporary (restore armor on effect expiry) D-38: Implement AoE damage distribution across enemies All 1069 tests pass. No files exceed 400 lines.
This commit is contained in:
@@ -89,7 +89,14 @@ export function applyEnemyDefenses(
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 2. Barrier absorption (percentage, spec §5.2) — skipped if bypassBarrier
|
||||
// 2a. Shield pool absorption (flat HP one-time pool, spec §5.1) — skipped if bypassBarrier
|
||||
if (!bypassBarrier && enemy.shieldPool && enemy.shieldPool > 0) {
|
||||
const absorb = Math.min(enemy.shieldPool, dmg);
|
||||
enemy.shieldPool -= absorb;
|
||||
dmg -= absorb;
|
||||
}
|
||||
|
||||
// 2b. Barrier absorption (percentage, spec §5.2) — skipped if bypassBarrier
|
||||
if (!bypassBarrier && enemy.barrier && enemy.barrier > 0) {
|
||||
dmg *= (1 - enemy.barrier);
|
||||
}
|
||||
@@ -135,6 +142,7 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
||||
|
||||
/** Mage barrier recharge rate (spec §5.2): 5% per tick */
|
||||
const MAGE_BARRIER_RECHARGE_RATE = 0.05;
|
||||
const MAGE_BARRIER_MAX = 0.4; // maxBarrier from MODIFIER_CONFIG.mage
|
||||
|
||||
/**
|
||||
* Apply mage barrier recharge (spec §5.2).
|
||||
@@ -160,45 +168,63 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
||||
defCtx: EnemyDefenseCtx,
|
||||
addLog: (msg: string) => void,
|
||||
) => {
|
||||
return (damage: number) => {
|
||||
return (damage: number, skipSpecials?: boolean) => {
|
||||
const rawMana = rawManaRef();
|
||||
const elements = elementsRef();
|
||||
let dmg = damage;
|
||||
|
||||
// Discipline specials (Executioner, Berserker) — before enemy defenses
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && ctx.combat.floorHP / ctx.combat.floorMaxHP < 0.25) {
|
||||
dmg *= 2;
|
||||
}
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
// Skipped for golem attacks (spec §9.4, D-04 fix)
|
||||
if (!skipSpecials) {
|
||||
// Executioner: per-enemy HP check (spec §4.4, D-15 fix)
|
||||
const executionerTarget = defCtx.enemy;
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.EXECUTIONER) && executionerTarget && executionerTarget.hp < executionerTarget.maxHP * 0.25) {
|
||||
dmg *= 2;
|
||||
}
|
||||
if (hasSpecial(effects, SPECIAL_EFFECTS.BERSERKER) && rawMana < maxMana * 0.5) {
|
||||
dmg *= 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply regular enemy defenses for ALL enemies (spec §5.2)
|
||||
dmg = defApply(dmg, defCtx.enemy, defCtx.roomType, addLog);
|
||||
|
||||
// Guardian-specific defensive pipeline (shield → barrier → health regen, spec §5.3)
|
||||
// Mage barrier recharge (spec §5.2) — after damage, recharge mage barrier on the enemy
|
||||
if (defCtx.enemy && defCtx.enemy.barrier && defCtx.enemy.barrier > 0 && defCtx.enemy.name.startsWith('Mage')) {
|
||||
const rechargedBarrier = Math.min(
|
||||
MAGE_BARRIER_MAX,
|
||||
defCtx.enemy.barrier + MAGE_BARRIER_RECHARGE_RATE * HOURS_PER_TICK,
|
||||
);
|
||||
// Update enemy barrier on the store (mutates via reference)
|
||||
defCtx.enemy.barrier = rechargedBarrier;
|
||||
}
|
||||
|
||||
// Guardian-specific defensive pipeline (spec §5.3, §11)
|
||||
const guardian = getGuardianForFloor(ctx.combat.currentFloor);
|
||||
if (guardian && (guardian.shield || guardian.barrier || guardian.healthRegen)) {
|
||||
if (guardian) {
|
||||
let shield = ctx.combat.guardianShield;
|
||||
const shieldMax = ctx.combat.guardianShieldMax;
|
||||
let barrier = ctx.combat.guardianBarrier;
|
||||
const barrierMax = ctx.combat.guardianBarrierMax;
|
||||
|
||||
if (guardian.shieldRegen && shield < shieldMax) {
|
||||
shield = Math.min(shieldMax, shield + guardian.shieldRegen * HOURS_PER_TICK);
|
||||
}
|
||||
if (guardian.barrierRegen && barrier < barrierMax) {
|
||||
barrier = Math.min(barrierMax, barrier + guardian.barrierRegen * HOURS_PER_TICK);
|
||||
}
|
||||
|
||||
// Shield absorption (flat pool, per-hit)
|
||||
if (shield > 0 && dmg > 0) {
|
||||
const absorb = Math.min(shield, dmg);
|
||||
shield -= absorb;
|
||||
dmg -= absorb;
|
||||
}
|
||||
|
||||
// Barrier reduction (percentage, per-hit)
|
||||
if (barrier > 0 && dmg > 0) {
|
||||
dmg *= (1 - barrier);
|
||||
}
|
||||
|
||||
// Guardian armor reduction (spec §11, D-26 fix)
|
||||
if (guardian.armor && guardian.armor > 0 && dmg > 0) {
|
||||
dmg *= (1 - guardian.armor);
|
||||
}
|
||||
|
||||
// Health regen reduces net damage (per-hit, already scaled by HOURS_PER_TICK)
|
||||
if (guardian.healthRegen && guardian.healthRegen > 0) {
|
||||
const healAmount = guardian.healthRegenIsPercent
|
||||
? Math.floor(ctx.combat.floorMaxHP * guardian.healthRegen / 100 * HOURS_PER_TICK)
|
||||
@@ -218,5 +244,40 @@ export function buildCombatCallbacks(params: BuildCombatCallbacksParams) {
|
||||
};
|
||||
};
|
||||
|
||||
return { onFloorCleared, makeOnDamageDealt, applyMageBarrierRecharge };
|
||||
// ─── Guardian Regen (once per tick, spec §5.3, D-25 fix) ─────────────────
|
||||
|
||||
/**
|
||||
* Apply guardian shield/barrier regen once per tick.
|
||||
* Must be called exactly once per combat tick, not per-damage-event.
|
||||
*/
|
||||
const applyGuardianRegen = () => {
|
||||
const guardian = getGuardianForFloor(ctx.combat.currentFloor);
|
||||
if (!guardian) return;
|
||||
|
||||
let shield = ctx.combat.guardianShield;
|
||||
const shieldMax = ctx.combat.guardianShieldMax;
|
||||
let barrier = ctx.combat.guardianBarrier;
|
||||
const barrierMax = ctx.combat.guardianBarrierMax;
|
||||
let changed = false;
|
||||
|
||||
if (guardian.shieldRegen && shield < shieldMax) {
|
||||
shield = Math.min(shieldMax, shield + guardian.shieldRegen * HOURS_PER_TICK);
|
||||
changed = true;
|
||||
}
|
||||
if (guardian.barrierRegen && barrier < barrierMax) {
|
||||
barrier = Math.min(barrierMax, barrier + guardian.barrierRegen * HOURS_PER_TICK);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
useCombatStore.setState({
|
||||
guardianShield: shield,
|
||||
guardianShieldMax: shieldMax,
|
||||
guardianBarrier: barrier,
|
||||
guardianBarrierMax: barrierMax,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { onFloorCleared, makeOnDamageDealt, applyMageBarrierRecharge, applyGuardianRegen };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user