✅ Lucifer Empathy intercept → reflection chain
| Category | Interaction |
| Status | Passing |
| Test | tests/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.
| AP | CP | Lane 0 | Lane 1 | Lane 2 | |
|---|---|---|---|---|---|
| P1 Demons | 6 | 0 | Lucifer (#088) 15 HP, 0 dmg, READIED | — | — |
| Flauros (#013) 15 HP, 0 dmg, READIED | |||||
| P2 Demons | 6 | 0 | Sabnock (#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:
| Demon | HP | Damage | State | Status Effects |
|---|---|---|---|---|
| Lucifer (#088) | 15 | 0 | EXHAUSTED | lucifers_empathy: 1 |
| Flauros (#013) | 15 | 0 | READIED | — |
| Sabnock (#003) | 15 | 0 | READIED | — |
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
| Check | Expected | Why |
|---|---|---|
| Lucifer damage | 2 | Intercepted Sabnock's attack (PWR=2, no DEF) |
| Flauros damage | 2 | Lucifer reflected 2 Fixed Damage (passive idx=1) |
| Sabnock state | EXHAUSTED | Attacked (basic attack exhausts) |
| P1 AP | 5 | 6 - 1 (Empathy cost) |
| P2 AP | 4 | 6 - 2 (attack cost) |
Mechanics Chained
-
Status-based tanking — lucifers_empathy status grants temporary target redirect (same pipeline as Marchosias permanent Field passive)
-
DAMAGE_RECEIVED event — fires after Lucifer takes the intercepted damage
-
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