Trigger on your own countered spell (Multani's Presence) - #4903
Conversation
"Whenever a spell you've cast is countered, draw a card." parsed to an Unknown trigger and was silently dropped, so Multani's Presence never fired. This is the passive dual of the already-supported active-side "a spell or ability you control counters a spell" trigger. Add a recognizer arm in `try_parse_player_trigger` that maps "whenever/when a spell you've cast|you control is countered" to the existing `TriggerMode::Countered`, gating the *countered* spell via `valid_card = Controller(You)`. At runtime `match_countered` evaluates `valid_card` against the `SpellCountered` event's `object_id` (the countered spell) through `target_filter_matches_object`, so the trigger fires only when the countered spell's controller is you — exactly the card's semantics. This differs from the active-side arm, which gates the countering source via `valid_source`. A spell's controller is its caster (CR 108.4), so "you've cast" and "you control" both route to the single `ControllerRef::You` filter. No new Effect/trigger mode or resolver machinery: reuses the existing `Countered` mode, its `match_countered` matcher, `valid_card` filtering, and the supported `Draw` child effect. Unlocks: Multani's Presence. Tests: - parser::oracle_trigger::tests::trigger_a_spell_youve_cast_is_countered - parser::oracle_trigger::tests::trigger_a_spell_you_control_is_countered - game::trigger_matchers::tests::countered_trigger_valid_card_gates_own_spell (runtime: own countered spell fires; opponent's countered spell does not) CR 701.6a (counter), CR 603.2 (trigger conditions), CR 108.4 (a spell's controller). Verified against docs/MagicCompRules.txt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements parser support and matching logic for triggers that fire when a spell you control or have cast is countered (such as Multani's Presence), along with corresponding unit tests. The review feedback correctly identifies that the comments in all three modified files cite CR 701.6a (token creation) instead of CR 701.5a (countering), violating the repository's rule on verified CR annotations.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| } | ||
| } | ||
|
|
||
| // CR 701.6a + CR 603.2 + CR 108.4: "Whenever a spell you've cast is |
There was a problem hiding this comment.
[HIGH] Incorrect CR citation for countering. Evidence: crates/engine/src/parser/oracle_trigger.rs:11712. Why it matters: The comment cites CR 701.6a (which defines token creation) instead of CR 701.5a (which defines countering). Suggested fix: Change CR 701.6a to CR 701.5a.
| // CR 701.6a + CR 603.2 + CR 108.4: "Whenever a spell you've cast is | |
| // CR 701.5a + CR 603.2 + CR 108.4: "Whenever a spell you've cast is |
References
- Every rules-touching line of engine code must carry a comment of the form CR : where the cited rule's body describes what the code is doing. (link)
|
|
||
| #[test] | ||
| fn countered_trigger_valid_card_gates_own_spell() { | ||
| // CR 701.6a + CR 108.4: Multani's Presence -- "Whenever a spell you've |
There was a problem hiding this comment.
[HIGH] Incorrect CR citation for countering. Evidence: crates/engine/src/game/trigger_matchers.rs:4682. Why it matters: The comment cites CR 701.6a (which defines token creation) instead of CR 701.5a (which defines countering). Suggested fix: Change CR 701.6a to CR 701.5a.
| // CR 701.6a + CR 108.4: Multani's Presence -- "Whenever a spell you've | |
| // CR 701.5a + CR 108.4: Multani's Presence -- "Whenever a spell you've |
References
- Every rules-touching line of engine code must carry a comment of the form CR : where the cited rule's body describes what the code is doing. (link)
|
|
||
| #[test] | ||
| fn trigger_a_spell_youve_cast_is_countered() { | ||
| // CR 701.6a + CR 108.4: Multani's Presence -- the passive dual of the |
There was a problem hiding this comment.
[HIGH] Incorrect CR citation for countering. Evidence: crates/engine/src/parser/oracle_trigger_tests.rs:8207. Why it matters: The comment cites CR 701.6a (which defines token creation) instead of CR 701.5a (which defines countering). Suggested fix: Change CR 701.6a to CR 701.5a.
| // CR 701.6a + CR 108.4: Multani's Presence -- the passive dual of the | |
| // CR 701.5a + CR 108.4: Multani's Presence -- the passive dual of the |
References
- Every rules-touching line of engine code must carry a comment of the form CR : where the cited rule's body describes what the code is doing. (link)
Parse changes introduced by this PR · 1 card(s), 2 signature(s) (baseline: main
|
matthewevans
left a comment
There was a problem hiding this comment.
Approved current head 58bd95b2abad18558b08505b586a8d921a688336.
Evidence checked: parse-diff is scoped to Multani's Presence (one card / two signatures), the parser-combinator gate passes locally, CI is green, and the implementation is at the existing TriggerMode::Countered seam. Hand trace: the parser maps "spell you've cast / spell controlled by you is countered" to valid_card = controller(You), and match_countered applies valid_card to the countered stack object while preserving the separate valid_source check for the countering spell/ability. The added matcher tests cover own-spell vs opponent-spell behavior and the parser test would fail if the trigger remained unimplemented. CR references used here were verified against the local rules text.
|
Hi @matthewevans — quick labeling question. The card implementations I've been landing (Regenerate, Role Reversal, Coastal Wizard, this countered-spell trigger, etc.) are coming through as |
Heya! If your PRs are being misclassified please let me know. In the case of adding new card support (wiring it up) that would be
Hope this helps! |
|
That's super helpful, thank you! Yes — these are all new-card wiring (Regenerate, Role Reversal, Coastal Wizard, the skip-step form, this countered-spell trigger, Valakut Fireboar), so |
Summary
Multani's Presence — "Whenever a spell you've cast is countered, draw a card." — parsed its trigger to
Unknown("Whenever a spell you've cast is countered")and silently dropped it. The engine already models this viaTriggerMode::Countered, but only the active side ("a spell or ability you control counters a spell") had a recognizer.Change
Add
parse_own_spell_countered_linetotry_parse_player_trigger(crates/engine/src/parser/oracle_trigger.rs) — the passive dual of the existing countering-side arm. It recognizes"whenever|when a spell you've cast|you control is countered"(whole-clauseall_consuming, nom combinators only) and emitsTriggerMode::Counteredwithvalid_card = Typed(controller = You).At runtime,
match_countered(game/trigger_matchers.rs) evaluatesvalid_cardagainst theSpellCounteredevent'sobject_id(the countered spell), so theYoucontroller filter restricts the trigger to your own countered spell — the passive dual of the active arm, which gates the countering source viavalid_source. TheDrawchild was already supported.Semantics: a spell's controller is its caster (CR 108.4), so "you've cast" ≡ "you control" for a spell; both route to
ControllerRef::You. The trigger fires only for your own countered spell, never an opponent's.CR annotations grep-verified: CR 701.6a (countering), CR 603.2 (trigger conditions), CR 108.4 (a spell's controller).
Tests
trigger_a_spell_youve_cast_is_countered/trigger_a_spell_you_control_is_countered— both forms parse toCounteredwithvalid_card = Controller(You),valid_source = None.countered_trigger_valid_card_gates_own_spell— runtime: your countered spell fires the trigger; an opponent's countered spell does NOT.Fails-before (trigger →
Unknown) / passes-after confirmed. Green: rustfmt, parser-combinator (Rule Zero) gate,clippy -D warnings, full engine suite (14546 passed / 0 failed),cargo coverageclean. The new arm is a whole-clauseall_consumingmatch on a clause that previously parsed toUnknown, so no other card's parse is affected.