[Critical] [Bug] Spire combat: 11 high-severity discrepancies including wrong dodge formula, missing guardian armor, broken elemental counters #333

Closed
opened 2026-06-08 16:02:47 +02:00 by Anexim · 2 comments
Owner

Spec: docs/specs/spire-combat-spec.md

Discrepancies found:

D-01 [HIGH] — Single global castProgress instead of per-weapon timers

  • Spec §3.1: Every spell on every weapon should have its own independent cast timer (weaponCastProgress: Record<instanceId, number>)
  • Code (combat-actions.ts): Uses a single scalar castProgress on CombatState. The multi-spell equipment path uses equipmentSpellStates[].castProgress, but the primary active spell uses one global counter.
  • Fix: Implement per-weapon cast progress records

D-04 [HIGH] — Golem attacks trigger Executioner/Berserker (spec says golems ignore)

  • Spec §9.4: "Golems ignore Executioner and Berserker discipline specials"
  • Code (golem-combat-actions.ts): Golem damage flows through onDamageDealt which applies Executioner and Berserker
  • Fix: Bypass onDamageDealt for golem attacks or add a flag to skip discipline specials

D-09 [HIGH] — Elemental counter lightning → earth contradicts spec lightning → water

  • Spec §4.2: lightning → water (lightning counters water), earth → lightning (earth counters lightning)
  • Code (elements.ts): lightning: 'earth' (lightning counters earth)
  • Fix: Reconcile — the spec and code have opposite directional counters for lightning

D-10 [HIGH] — Composite element counters incomplete

  • Spec §4.2: blackflame counters frost/water/light (bidirectional), radiantflames counters frost/water/dark (bidirectional)
  • Code (ELEMENT_OPPOSITES): Only defines blackflame: 'light' and radiantflames: 'dark' — missing frost and water interactions
  • Fix: Add full composite counter table to ELEMENT_OPPOSITES

D-15 [HIGH] — Executioner checks floor-level HP, not per-enemy HP

  • Spec §4.4: "Enemy HP < 25% of maxHP" (per-enemy check)
  • Code (combat-tick.ts:183): Checks ctx.combat.floorHP / ctx.combat.floorMaxHP < 0.25 (floor aggregate)
  • Fix: Change to per-enemy HP check

D-20 [HIGH] — Dodge formula differs massively from spec

  • Spec §5.1: dodgeChance = min(0.55, floor × 0.003) — starts at 0
  • Code (enemy-generator.ts): baseDodge = 0.20, dodgePerFloor = 0.004 — starts at 0.20
  • Impact: At floor 12, code gives 0.248 dodge vs spec's 0.036 — 7× difference
  • Fix: Change to match spec formula

D-22 [HIGH] — Shield modifier sets percentage barrier, not flat one-time pool

  • Spec §5.1: Shield = "One-time barrier pool = 15% of maxHP" (flat HP pool)
  • Code: Shield sets barrier = 0.15 (percentage reduction), identical to mage modifier
  • Fix: Implement flat shield pool that absorbs damage and depletes

D-23 [HIGH] — applyMageBarrierRecharge defined but never called (dead code)

  • File: combat-tick.ts:173-179
  • Function exists but is never called from buildCombatCallbacks or makeOnDamageDealt
  • Fix: Wire up barrier recharge in the damage pipeline

D-25 [HIGH] — Guardian regen fires per-damage-event, not per-tick

  • Spec §5.3: Shield/barrier/health regen should tick once per combat tick
  • Code (combat-tick.ts:194-198): Regen applied inside per-hit onDamageDealt callback — fires multiple times per tick if multiple spells hit
  • Fix: Move regen to once-per-tick processing

D-26 [HIGH] — Guardian armor never applied in pipeline

  • Spec §11: Guardian armor should be checked in guardian pipeline
  • Code: GuardianDef.armor is defined in data but never applied in makeOnDamageDealt
  • Fix: Add armor reduction step to guardian defensive pipeline

D-31 [HIGH] — armor_corrode permanently reduces armor with no restore

  • Spec §6.3: "reduces armor value by % for duration" (temporary)
  • Code (dot-runtime.ts:115): enemyArmor = Math.max(0, enemyArmor - effect.magnitude) — permanently reduces with no restore on expire
  • Fix: Store original armor and restore when effect expires, or use effectiveArmor separately

D-38 [MEDIUM] — AoE spells damage all enemies fully; aoeTargets unused

  • Spec §3.2: AoE distributes damage across enemies
  • Code (combat-damage.ts): Every enemy takes full damage. aoeTargets field stored but never used.
  • Fix: Implement damage distribution or cap based on aoeTargets
**Spec:** `docs/specs/spire-combat-spec.md` **Discrepancies found:** ### D-01 [HIGH] — Single global `castProgress` instead of per-weapon timers - **Spec §3.1:** Every spell on every weapon should have its own independent cast timer (`weaponCastProgress: Record<instanceId, number>`) - **Code (`combat-actions.ts`):** Uses a single scalar `castProgress` on `CombatState`. The multi-spell equipment path uses `equipmentSpellStates[].castProgress`, but the primary active spell uses one global counter. - **Fix:** Implement per-weapon cast progress records ### D-04 [HIGH] — Golem attacks trigger Executioner/Berserker (spec says golems ignore) - **Spec §9.4:** "Golems ignore Executioner and Berserker discipline specials" - **Code (`golem-combat-actions.ts`):** Golem damage flows through `onDamageDealt` which applies Executioner and Berserker - **Fix:** Bypass `onDamageDealt` for golem attacks or add a flag to skip discipline specials ### D-09 [HIGH] — Elemental counter `lightning → earth` contradicts spec `lightning → water` - **Spec §4.2:** `lightning → water` (lightning counters water), `earth → lightning` (earth counters lightning) - **Code (`elements.ts`):** `lightning: 'earth'` (lightning counters earth) - **Fix:** Reconcile — the spec and code have opposite directional counters for lightning ### D-10 [HIGH] — Composite element counters incomplete - **Spec §4.2:** blackflame counters frost/water/light (bidirectional), radiantflames counters frost/water/dark (bidirectional) - **Code (`ELEMENT_OPPOSITES`):** Only defines `blackflame: 'light'` and `radiantflames: 'dark'` — missing frost and water interactions - **Fix:** Add full composite counter table to `ELEMENT_OPPOSITES` ### D-15 [HIGH] — Executioner checks floor-level HP, not per-enemy HP - **Spec §4.4:** "Enemy HP < 25% of maxHP" (per-enemy check) - **Code (`combat-tick.ts:183`):** Checks `ctx.combat.floorHP / ctx.combat.floorMaxHP < 0.25` (floor aggregate) - **Fix:** Change to per-enemy HP check ### D-20 [HIGH] — Dodge formula differs massively from spec - **Spec §5.1:** `dodgeChance = min(0.55, floor × 0.003)` — starts at 0 - **Code (`enemy-generator.ts`):** `baseDodge = 0.20`, `dodgePerFloor = 0.004` — starts at 0.20 - **Impact:** At floor 12, code gives 0.248 dodge vs spec's 0.036 — **7× difference** - **Fix:** Change to match spec formula ### D-22 [HIGH] — Shield modifier sets percentage barrier, not flat one-time pool - **Spec §5.1:** Shield = "One-time barrier pool = 15% of maxHP" (flat HP pool) - **Code:** Shield sets `barrier = 0.15` (percentage reduction), identical to mage modifier - **Fix:** Implement flat shield pool that absorbs damage and depletes ### D-23 [HIGH] — `applyMageBarrierRecharge` defined but never called (dead code) - **File:** `combat-tick.ts:173-179` - Function exists but is never called from `buildCombatCallbacks` or `makeOnDamageDealt` - **Fix:** Wire up barrier recharge in the damage pipeline ### D-25 [HIGH] — Guardian regen fires per-damage-event, not per-tick - **Spec §5.3:** Shield/barrier/health regen should tick once per combat tick - **Code (`combat-tick.ts:194-198`):** Regen applied inside per-hit `onDamageDealt` callback — fires multiple times per tick if multiple spells hit - **Fix:** Move regen to once-per-tick processing ### D-26 [HIGH] — Guardian armor never applied in pipeline - **Spec §11:** Guardian armor should be checked in guardian pipeline - **Code:** `GuardianDef.armor` is defined in data but never applied in `makeOnDamageDealt` - **Fix:** Add armor reduction step to guardian defensive pipeline ### D-31 [HIGH] — `armor_corrode` permanently reduces armor with no restore - **Spec §6.3:** "reduces armor value by % for duration" (temporary) - **Code (`dot-runtime.ts:115`):** `enemyArmor = Math.max(0, enemyArmor - effect.magnitude)` — permanently reduces with no restore on expire - **Fix:** Store original armor and restore when effect expires, or use `effectiveArmor` separately ### D-38 [MEDIUM] — AoE spells damage all enemies fully; `aoeTargets` unused - **Spec §3.2:** AoE distributes damage across enemies - **Code (`combat-damage.ts`):** Every enemy takes **full** damage. `aoeTargets` field stored but never used. - **Fix:** Implement damage distribution or cap based on `aoeTargets`
Anexim added the ai:todo label 2026-06-08 16:02:47 +02:00
n8n-gitea was assigned by Anexim 2026-06-08 16:02:47 +02:00
Anexim added ai:in-progress and removed ai:todo labels 2026-06-08 16:39:02 +02:00
Author
Owner

Starting work on issue #333. Reading spec and all affected source files to build a complete picture before implementing fixes.

Starting work on issue #333. Reading spec and all affected source files to build a complete picture before implementing fixes.
Author
Owner

All 12 fixes implemented and pushed to master (commit 098ec86):

D-01 Per-weapon cast progress via weaponCastProgress: Record<string, number> on CombatState
D-04 Added skipSpecials parameter to onDamageDealt; golem attacks pass true to bypass Executioner/Berserker
D-09 Fixed ELEMENT_OPPOSITES to use arrays; lightning now correctly counters water (not earth)
D-10 Full composite counter table: blackflame↔frost/water/light, radiantflames↔frost/water/dark (all bidirectional)
D-15 Executioner now checks defCtx.enemy.hp < enemy.maxHP * 0.25 instead of floorHP ratio
D-20 Dodge formula: baseDodge: 0, dodgePerFloor: 0.003 (starts at 0, not 0.20)
D-22 Shield modifier sets shieldPool (flat HP = 15% maxHP) instead of barrier (percentage)
D-23 Mage barrier recharge wired into onDamageDealt callback (runs per damage event)
D-25 Guardian shield/barrier regen moved to applyGuardianRegen() called once per tick
D-26 Guardian armor reduction added to the guardian defensive pipeline
D-31 armor_corrode now accumulates magnitude and computes effectiveArmor from base armor (temporary)
D-38 AoE damage distributed across up to aoeTargets enemies (focus-fire: lowest HP first)

All 1069 tests pass. No files exceed 400 lines.

All 12 fixes implemented and pushed to master (commit 098ec86): **D-01** ✅ Per-weapon cast progress via `weaponCastProgress: Record<string, number>` on CombatState **D-04** ✅ Added `skipSpecials` parameter to `onDamageDealt`; golem attacks pass `true` to bypass Executioner/Berserker **D-09** ✅ Fixed `ELEMENT_OPPOSITES` to use arrays; lightning now correctly counters water (not earth) **D-10** ✅ Full composite counter table: blackflame↔frost/water/light, radiantflames↔frost/water/dark (all bidirectional) **D-15** ✅ Executioner now checks `defCtx.enemy.hp < enemy.maxHP * 0.25` instead of floorHP ratio **D-20** ✅ Dodge formula: `baseDodge: 0, dodgePerFloor: 0.003` (starts at 0, not 0.20) **D-22** ✅ Shield modifier sets `shieldPool` (flat HP = 15% maxHP) instead of `barrier` (percentage) **D-23** ✅ Mage barrier recharge wired into `onDamageDealt` callback (runs per damage event) **D-25** ✅ Guardian shield/barrier regen moved to `applyGuardianRegen()` called once per tick **D-26** ✅ Guardian armor reduction added to the guardian defensive pipeline **D-31** ✅ `armor_corrode` now accumulates magnitude and computes `effectiveArmor` from base armor (temporary) **D-38** ✅ AoE damage distributed across up to `aoeTargets` enemies (focus-fire: lowest HP first) All 1069 tests pass. No files exceed 400 lines.
Anexim added ai:done and removed ai:in-progress labels 2026-06-08 18:27:42 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Anexim/Mana-Loop#333