fix(engine): tap target defending player controls for equipment attack triggers (#6678) - #6751
Conversation
…tack triggers (phase-rs#6678) Captain America's Shield's "Whenever equipped creature attacks, tap target creature defending player controls" prompted for a target but never tapped it. The trigger source is the Equipment, not the attacker, so `capture_combat_status` records `defending_player: None` on the Shield's trigger-source snapshot. Three sites resolved "defending player" as `trigger_source.map(|s| s.combat_status.defending_player).unwrap_or_else(fallback)`. When `trigger_source` is `Some` but the captured value is `None`, `.map()` yields `Some(None)`, so the `resolve_defending_player` fallback never runs — the chosen target then failed the CR 608.2b resolution-time legality re-check and the ability silently fizzled. Regression from the P04 resolution-frames refactor (777dbab). CR 508.5 / 508.5a: when an ability refers to both an attacking creature and a defending player, the defender is the player that creature is attacking. A captured `None` means "this source is not the attacker" (Equipment/Aura), not "no defender" — fall through to `resolve_defending_player`, which reads the attacker from the triggering event. - filter.rs: route the inline `DefendingPlayer` controller arm in `filter_inner_for_object` (the load-bearing target-validation path) through the single-authority `source_defending_player`, eliminating the duplicate. - filter.rs `source_defending_player`: `.and_then().or_else()` so a captured `None` falls through to the attacker fallback. - quantity.rs `source_defending_player_for_context`: same fix. Fixes the whole "equipped creature attacks -> affect defending player's permanents" class (Greatsword of Tyr, Mjolnir, Thunder Lasso, etc.). Adds a discriminating integration test driving the real declare-attackers / trigger pipeline; it fails on the prior build and passes with the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe change updates defending-player resolution for triggered attachment sources and adds an integration regression test covering Captain America’s Shield tapping a defending creature after an equipped creature attacks. ChangesDefending Player Resolution
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1⚔️ Resolve merge conflicts 💡
🧪 Generate unit tests (beta)
Comment |
Preserve the contributor's defending-player resolution fix while retaining main's sorted integration module registrations. Co-authored-by: Jacob Woodson <38709105+JacobWoodson@users.noreply.github.com>
Parse changes introduced by this PR✓ No card-parse changes detected. |
|
Maintainer hold update for current head |
matthewevans
left a comment
There was a problem hiding this comment.
Request changes — the quantity.rs fallback extends the fix without a discriminating runtime test.
🔴 Blocker
[MED] Missing discriminating runtime proof for the QuantityContext defending-player fallback. Evidence: crates/engine/src/game/quantity.rs:157-165 changes source_defending_player_for_context to fall through from a captured None; its current production consumers include the attachment-controller path at quantity.rs:4208-4210 and the damage-source-controller path at quantity.rs:4277-4279. The new Shield regression at crates/engine/tests/integration/issue_6678_captain_america_shield_tap_defender.rs:94-130 only exercises target legality through filter.rs, so restoring only the quantity.rs .and_then(...).or_else(...) fallback leaves that test green. Why it matters: the expanded quantity behavior can regress independently while the reported Shield path still passes. Suggested fix: add a focused attachment-trigger, current-combat runtime test that reaches one of those quantity consumers and fails when this fallback is reverted, or remove the unproven quantity.rs change.
✅ Clean
All CI checks are green and the current-head parse-diff reports no card-parse changes; neither establishes the independent quantity route above.
Recommendation: request changes — supply the focused runtime proof or narrow the patch.
Address review (matthewevans): drop the quantity.rs `source_defending_player_for_context` fallback change. Its only consumers are the `AttachmentsOnLeavingObject` count and `damage_source_controller_matches` paths, which the Shield regression test does not exercise, so the change was unproven by a discriminating test. Neither consumer maps to a real card in the "equipped creature attacks -> defending player" class, so a runtime test would build for a phantom card. Restore quantity.rs to baseline; the filter.rs fix (covered by the discriminating Shield test) stands alone. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
matthewevans
left a comment
There was a problem hiding this comment.
Approved: the narrowed filter.rs fix delegates DefendingPlayer lookup to the existing source_defending_player authority, and the Captain America's Shield integration test exercises the real combat-trigger target-validation and resolution pipeline.
|
@matthewevans thanks — you're right that the Rationale for removing rather than adding a test: the two consumers you identified — The PR now touches exactly three files: the |
Fixes #6678
Problem
Captain America's Shield's attack trigger — "Whenever equipped creature attacks, tap target creature defending player controls" — prompted for a target but never tapped it. Reported as a regression in 0.38.0.
Root cause
The trigger source is the Equipment, not the attacker.
capture_combat_statusfinds the Shield absent fromcombat.attackersand recordsdefending_player: Noneon its trigger-source snapshot.Three sites resolved "defending player" with the shape:
trigger_source .map(|s| s.combat_status.defending_player) .unwrap_or_else(|| resolve_defending_player(state, source_id))When
trigger_sourceisSomebut the captureddefending_playerisNone,.map()yieldsSome(None), so.unwrap_or_else()never runs the fallback and the whole expression returnsNone. With no defending player resolvable, the chosen target failed the CR 608.2b resolution-time legality re-check and the ability silently fizzled — the prompt appeared, nothing tapped.Introduced by the P04 resolution-frames refactor (
777dbabf), matching the reported regression window.The load-bearing site was an inline duplicate of the resolver in
filter_inner_for_object(the target-validation path). A parallel duplicate inquantity.rshad the same defect.Fix (CR 508.5 / 508.5a)
When an ability refers to both an attacking creature and a defending player, the defender is the player that creature is attacking (CR 508.5), individually determined per attacker (CR 508.5a). A captured
Nonemeans "this source is not the attacker" (Equipment/Aura) — not "no defender" — so it must fall through toresolve_defending_player, which reads the attacker from the triggering event.filter.rs— route the inlineControllerRef::DefendingPlayerarm infilter_inner_for_objectthrough the single-authoritysource_defending_player(mirroring how the siblingSourceChosenPlayerarm already delegates), eliminating the duplicate.filter.rssource_defending_player—.and_then().or_else()so a capturedNonefalls through to the attacker fallback.quantity.rssource_defending_player_for_context— same fix.This fixes the whole "equipped creature attacks → affect something the defending player controls" class (Greatsword of Tyr, Mjölnir Storm Hammer, Thunder Lasso, Heart-Piercer Bow, and the rest listed in the issue), not just the Shield.
Testing
issue_6678_captain_america_shield_tap_defender.rsdrives the real declare-attackers / trigger pipeline (not a synthetic trigger event). It fails on the prior build — and even with only the parallel-path fix — and passes once the inline arm is fixed.cargo test -p engine: 3830 integration + 498 lib tests pass, 0 failures. The self-attack path (Kibo, Uktabi Prince — capturedSome(defender)) still passes.cargo clippy -p engine --tests: clean.docs/MagicCompRules.txt.Acceptance criteria
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests