Skip to main content

✅ Lucifer Empathy intercept → reflection chain

CategoryInteraction
StatusPassing
Testtests/test_abilities_complex.py::TestLuciferEmpathy::test_empathy_intercept_then_reflect

Verifies the full tank-then-reflect chain: Empathy redirects an attack to Lucifer, Lucifer takes damage, Field passive reflects that damage to a local demon. Two mechanics chained end-to-end.

Preconditions

Phase: P1 Main Phase. No status effects active.

APCPLane 0Lane 1Lane 2
P1 Demons60Lucifer (#088) 15 HP, 0 dmg, READIED
Flauros (#013) 15 HP, 0 dmg, READIED
P2 Demons60Sabnock (#003) 15 HP, 0 dmg, PWR=2, READIED

Step-by-Step Walkthrough

Step 1: Lucifer activates Empathy

P1 activates Lucifer's Empathy (idx=0, Quick, 1 AP, exhaust, 1x).

# Setup
lucifer = make_demon("088", lane=0, owner=Side.PLAYER_1)
flauros = make_demon("013", lane=0, owner=Side.PLAYER_1)
sabnock = make_demon("003", lane=0, owner=Side.PLAYER_2)
state, placed = _make_state_with_demons(lucifer, flauros, sabnock, ap=6)
lucifer_p, flauros_p, sabnock_p = placed

# Step 1: Lucifer uses Empathy
state = execute_ability(state, lucifer_p, ability_idx=0)

What the engine does:

  • AP deducted: P1 AP 6 → 5

  • Exhaust: Lucifer becomes EXHAUSTED

  • Handler: applies lucifers_empathy status (value=1) to Lucifer

  • Status expires: end of current main phase

State after Step 1:

DemonHPDamageStateStatus Effects
Lucifer (#088)150EXHAUSTEDlucifers_empathy: 1
Flauros (#013)150READIED
Sabnock (#003)150READIED

Step 2: P2 attacks Flauros — full resolution chain

P2 declares attack: Sabnock (#003) → Flauros (#013).

# Step 2: P2 attacks Flauros — Lucifer intercepts, takes damage, reflects
rng = DeterministicRNG(42)
state = execute_action(
state, "attack",
{"demon_id": sabnock_p.instance_id, "target_id": flauros_p.instance_id},
rng,
)

2a. Marchosias/Empathy target redirect check

Before deal_damage, the engine calls apply_marchosias_target_redirect:

  • Scans for demons with unit_id == "087" (Marchosias) OR lucifers_empathy status > 0

  • Finds Lucifer in lane 0 with lucifers_empathy status active

  • Checks: Lucifer is allied with Flauros ✅, same lane ✅, not fatally wounded ✅, not already the target ✅, in range from Sabnock ✅

  • Redirect: target changes from Flauros → Lucifer

2b. Damage dealt to Lucifer

deal_damage(state, sabnock, lucifer, pwr=2) resolves:

  • Lucifer has no DEF passive → effective damage = max(0, 2 - 0) = 2

  • Lucifer damage: 0 → 2

  • 2 < 15 HP → not fatally wounded

  • DAMAGE_RECEIVED event fires: {target: Lucifer, value: 2}

2c. Lucifer's Field passive triggers on DAMAGE_RECEIVED

_lucifer_damage_reflection handler fires (registered on EventType.DAMAGE_RECEIVED for unit "088"):

  • Checks: event target IS Lucifer ✅, value=2 > 0 ✅, Lucifer not fatally wounded ✅

  • Finds eligible local targets: demons in lane 0, not Lucifer, not fatally wounded, remaining HP ≥ 1

  • Eligible: Flauros (15 HP - 0 damage = 15 ≥ 1) and Sabnock (15 HP - 0 damage = 15 ≥ 1)

  • Picks first eligible: Flauros

  • deal_fixed_damage(state, flauros, 2) → Flauros damage: 0 → 2

2d. Attack cleanup

  • Sabnock becomes EXHAUSTED

  • P2 AP deducted: 6 → 4 (attack costs 2 AP)

Expected Postconditions

CheckExpectedWhy
Lucifer damage2Intercepted Sabnock's attack (PWR=2, no DEF)
Flauros damage2Lucifer reflected 2 Fixed Damage (passive idx=1)
Sabnock stateEXHAUSTEDAttacked (basic attack exhausts)
P1 AP56 - 1 (Empathy cost)
P2 AP46 - 2 (attack cost)

Mechanics Chained

  1. Status-based tanking — lucifers_empathy status grants temporary target redirect (same pipeline as Marchosias permanent Field passive)

  2. DAMAGE_RECEIVED event — fires after Lucifer takes the intercepted damage

  3. Damage reflection — Lucifer's Field passive (idx=1) responds to DAMAGE_RECEIVED and deals Fixed Damage to a local demon

If any link breaks — redirect doesn't fire, event doesn't propagate, reflection doesn't trigger — the test fails.

Assertions

lucifer_after = next(d for d in state.demons if d.unit_id == "088")
flauros_after = next(d for d in state.demons if d.unit_id == "013")
sabnock_after = next(d for d in state.demons if d.unit_id == "003")

# Lucifer intercepted: took 2 damage
assert lucifer_after.damage == 2
# Lucifer reflected: Flauros took 2 Fixed Damage
assert flauros_after.damage == 2
# Sabnock exhausted from attacking
assert sabnock_after.state == DemonState.EXHAUSTED