Skip to content

Commit fc114ed

Browse files
liamiak1claude
andcommitted
Require a grant to reach the zone it is being used in
A MayPlay grant clears the restriction zone, and checkZoneRestrictions then returned true for any null zone. So a grant that only reduces a cost reached cards it was never meant to: with As Foretold out, an opponent's hand was castable. MayPlayDontGrantZonePermissions marks a cost reduction rather than permission, so it is not enough on its own, but it must not get in the way when a second grant does allow the zone - Grenzo pays {0} for cards it does not own, and Heist is what lets them be cast at all. The card reaching this point is a copy with no play options left, so the check reads them off the real card, the same way the branch below it does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 89e69d8 commit fc114ed

1 file changed

Lines changed: 28 additions & 15 deletions

File tree

forge-game/src/main/java/forge/game/spellability/SpellAbilityRestriction.java

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818
package forge.game.spellability;
1919

20-
import java.util.List;
2120
import java.util.Map;
2221
import java.util.function.Predicate;
2322

@@ -232,27 +231,28 @@ public final boolean checkZoneRestrictions(final Card c, final SpellAbility sa)
232231
if (sa.isSpell()) {
233232
final CardPlayOption o = c.mayPlay(sa.getMayPlay());
234233
if (o == null || sa.isCastFromPlayEffect()) {
235-
return this.getZone() == null || (cardZone != null && cardZone.is(this.getZone()));
234+
if (this.getZone() != null) {
235+
return cardZone != null && cardZone.is(this.getZone());
236+
}
237+
// A cleared zone means a grant is what allows this, so for someone else's card
238+
// some grant has to reach the zone it is in. The card here is a copy and has
239+
// no play options left, so they come off the real one.
240+
if (sa.isCastFromPlayEffect() || activator == c.getOwner()) {
241+
return true;
242+
}
243+
return hasZoneGrantor(activator.getGame().getCardState(c, null), activator);
236244
} else if (o.getPlayer() == activator) {
237245
Map<String,String> params = sa.getMayPlay().getMapParams();
238246

239247
// NOTE: this assumes that it's always possible to cast cards from hand and you don't
240248
// need special permissions for that. If WotC ever prints a card that forbids casting
241249
// cards from hand, this may become relevant.
242250
if (!o.grantsZonePermissions() && cardZone != null && (!cardZone.is(ZoneType.Hand) || activator != c.getOwner())) {
243-
final List<CardPlayOption> opts = c.mayPlay(activator);
244-
boolean hasOtherGrantor = false;
245-
for (CardPlayOption opt : opts) {
246-
if (opt.grantsZonePermissions()) {
247-
hasOtherGrantor = true;
248-
break;
249-
}
250-
}
251251
if (cardZone.is(ZoneType.Graveyard) && sa.isAftermath()) {
252252
// Special exclusion for Aftermath, useful for e.g. As Foretold
253253
return true;
254254
}
255-
if (!hasOtherGrantor) {
255+
if (!hasZoneGrantor(c, activator)) {
256256
return false;
257257
}
258258
}
@@ -280,10 +280,10 @@ public final boolean checkZoneRestrictions(final Card c, final SpellAbility sa)
280280
return false;
281281
}
282282

283-
// A zone restriction means the activator's own zone. CR 109.5: a card outside the
284-
// battlefield has no controller, so the "you" in "your graveyard" is its owner.
285-
// Shaman's Trance makes every graveyard count as the activator's.
286-
if (sa.isSpell() && activator != c.getOwner() && this.getZone() != null
283+
// Reaching here means the card is in a zone of the restricted type, and that has to be
284+
// the activator's own. CR 109.5: a card outside the battlefield has no controller, so the
285+
// "you" in "your graveyard" is its owner. Shaman's Trance makes every graveyard theirs.
286+
if (sa.isSpell() && activator != c.getOwner()
287287
&& !(this.getZone() == ZoneType.Graveyard
288288
&& activator.hasKeyword("Shaman's Trance"))) {
289289
return false;
@@ -292,6 +292,19 @@ public final boolean checkZoneRestrictions(final Card c, final SpellAbility sa)
292292
return true;
293293
}
294294

295+
/** Whether any play option the activator has on this card reaches the zone it is in. */
296+
private static boolean hasZoneGrantor(final Card c, final Player activator) {
297+
if (c == null) {
298+
return false;
299+
}
300+
for (CardPlayOption opt : c.mayPlay(activator)) {
301+
if (opt.grantsZonePermissions()) {
302+
return true;
303+
}
304+
}
305+
return false;
306+
}
307+
295308
/**
296309
* <p>
297310
* checkTimingRestrictions.

0 commit comments

Comments
 (0)