Skip to content

Implement miracle-granting cards - #15591

Open
matoro wants to merge 4 commits into
magefree:masterfrom
matoro:grantmiracle
Open

Implement miracle-granting cards#15591
matoro wants to merge 4 commits into
magefree:masterfrom
matoro:grantmiracle

Conversation

@matoro

@matoro matoro commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

This implements engine support for, and the individual cards of, the subset of 3 cards that dynamically grant miracle to other cards in hand.

My first draft of this was relatively straightforward, but the hardest problem was specifically the interaction between Aminatou and X-cost spells. We can't apply our cost modifications prior to calling controller.cast(), as we don't yet know the value of X. So they must be applied in the form of a continuous effect. But that cost reduction effect must return true for applies() only for the specific card that has been drawn. Thus we have to dynamically create the effect, insert it into the effects list, cast the spell, and then remove the effect.

This watcher is unique in that it adds a MiracleAbility inside of its watch() function, which is a class which adds a watcher to the global list when instantiated. Since watch() is called while iterating over each watcher, dynamically adding a new Watcher triggers a ConcurrentModificationException.

To work around this, I've introduced a new isolated flag to watchers. When this flag is set, a copy of the watcher is made, watch() called on that, and the resulting changes merged back to the Watchers map. This flag defaults to unset, in which case the old behavior is retained. It should only be necessary to grant this flag for watchers which may possible add additonal watchers to the global map inside their watch() code.

The second major change is that when control of a permanent is changed, it also updates the controllerId (and sourceId, when that control change is due to the card becoming a new permanent e.g. blinking) of all associated watchers in the global map. This is necessary because the watchers associated with an ability (via Ability.getWatchers()) are only used for instantiating new watchers, and it doesn't apply to watchers which were created by old instances of the permanent.

This should help improve some possible bugs with control-changing effects on watchers, especially CARD-scoped watchers (see #13774)

The third change is that object-copying code now supports copying arbitrary serializable objects by serializing them to a byte array and back. It is possible to serialize lambdas, which is why this was necessary since we need to instantiate a new instance of an effect on the fly.

matoro added 4 commits July 10, 2026 16:51
Engine support for a watcher that dynamically grants miracle to cards as
they are drawn.

This watcher is unique in that it adds a `MiracleAbility` inside of its
`watch()` function, which is a class which adds a watcher to the global
list when instantiated.  Since `watch()` is called while iterating over
each watcher, dynamically adding a new `Watcher` triggers a
`ConcurrentModificationException`.

To work around this, I've introduced a new `isolated` flag to watchers.
When this flag is set, a copy of the watcher is made, `watch()` called
on that, and the resulting changes merged back to the `Watchers` map.
This flag defaults to unset, in which case the old behavior is retained.
It should only be necessary to grant this flag for watchers which may
possible add additonal watchers to the global map inside their `watch()`
code.

The second major change is that when control of a permanent is changed,
it also updates the `controllerId` (and `sourceId`, when that control
change is due to the card becoming a new permanent e.g. blinking) of all
associated watchers in the global map.  This is necessary because the
watchers associated with an ability (via `Ability.getWatchers()`) are
only used for instantiating new watchers, and it doesn't apply to
watchers which were created by old instances of the permanent.

This should help improve some possible bugs with control-changing
effects on watchers, especially CARD-scoped watchers (see
magefree#13774)

The third change is that object-copying code now supports copying
arbitrary serializable objects by serializing them to a byte array and
back.  It is possible to serialize lambdas, which is why this was
necessary since we need to instantiate a new instance of an effect on
the fly.
@matoro

matoro commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

[[Molecule Man]] [[Lorehold, the Historian]] [[Aminatou, Veil Piercer]]

@github-actions

Copy link
Copy Markdown

Molecule Man - (Gatherer) (Scryfall) (EDHREC)

{6}
Legendary Creature — Human Villain
5/5
Nonland cards in your hand have miracle {0}. (You may cast a card for its miracle cost when you draw it if it's the first card you drew this turn.)

Lorehold, the Historian - (Gatherer) (Scryfall) (EDHREC)

{3}{R}{W}
Legendary Creature — Elder Dragon
5/5
Flying, haste
Each instant and sorcery card in your hand has miracle {2}. (You may cast a card for its miracle cost when you draw it if it's the first card you drew this turn.)
At the beginning of each opponent's upkeep, you may discard a card. If you do, draw a card.

Aminatou, Veil Piercer - (Gatherer) (Scryfall) (EDHREC)

{1}{W}{U}{B}
Legendary Creature — Human Wizard
2/4
At the beginning of your upkeep, surveil 2. (Look at the top two cards of your library, then put any number of them into your graveyard and the rest on top of your library in any order.)
Each enchantment card in your hand has miracle. Its miracle cost is equal to its mana cost reduced by {4}. (You may cast a card for its miracle cost when you draw it if it's the first card you drew this turn.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant