Fixing copy and cast from exile effects (#10436)

* Added unit test for magefree/mage#10435

* Added test for potential breakage of prosper functionality

* Copies of cards are now created in the right zone

* Added PlayCardTriggeredAbility

This triggered ability checks to make sure a card was actually played (as opposed to a copy of a card).
Common abilities have been refactored to use this new ability

* Added mizzix's mastery overload test

* Fixed Mizzix's mastery overload

* Added new ability to Juju Bubble

---------

Co-authored-by: xenohedron <xenohedron@users.noreply.github.com>
This commit is contained in:
Alexander Novotny 2023-06-08 18:32:59 -07:00 committed by GitHub
parent 2f79343bc8
commit a0f8a42699
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 337 additions and 225 deletions

View file

@ -99,8 +99,6 @@ class ArcaneBombardmentEffect extends OneShotEffect {
Cards copies = new CardsImpl();
for (Card card : exileZone.getCards(game)) {
Card copiedCard = game.copyCard(card, source, source.getControllerId());
game.getExile().add(source.getSourceId(), "", copiedCard);
game.getState().setZone(copiedCard.getId(), Zone.EXILED);
copies.add(copiedCard);
}
for (Card copiedCard : copies.getCards(game)) {

View file

@ -160,8 +160,6 @@ class EliteArcanistCopyEffect extends OneShotEffect {
if (controller != null) {
Card copiedCard = game.copyCard(imprintedInstant, source, source.getControllerId());
if (copiedCard != null) {
game.getExile().add(source.getSourceId(), "", copiedCard);
game.getState().setZone(copiedCard.getId(), Zone.EXILED);
if (controller.chooseUse(Outcome.PlayForFree, "Cast the copied card without paying mana cost?", source, game)) {
game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE);
Boolean cardWasCast = controller.cast(controller.chooseAbilityForCast(copiedCard, game, true),

View file

@ -103,7 +103,6 @@ class GodEternalKefnetDrawCardReplacementEffect extends ReplacementEffectImpl {
blueprint.addAbility(new SimpleStaticAbility(Zone.ALL, new SpellCostReductionSourceEffect(2)));
}
Card copiedCard = game.copyCard(blueprint, source, source.getControllerId());
you.moveCardToHandWithInfo(copiedCard, source, game, true); // The copy is created in and cast from your hand. (2019-05-03)
game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE);
you.cast(you.chooseAbilityForCast(copiedCard, game, false), game, false, new ApprovingObject(source, game));
game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), null);

View file

@ -139,8 +139,6 @@ class IsochronScepterCopyEffect extends OneShotEffect {
if (controller.chooseUse(outcome, "Create a copy of " + imprintedInstant.getName() + '?', source, game)) {
Card copiedCard = game.copyCard(imprintedInstant, source, source.getControllerId());
if (copiedCard != null) {
game.getExile().add(source.getSourceId(), "", copiedCard);
game.getState().setZone(copiedCard.getId(), Zone.EXILED);
if (controller.chooseUse(outcome, "Cast the copied card without paying mana cost?", source, game)) {
if (copiedCard.getSpellAbility() != null) {
game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE);

View file

@ -1,7 +1,7 @@
package mage.cards.j;
import java.util.UUID;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.PlayCardTriggeredAbility;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.mana.ManaCostsImpl;
@ -11,9 +11,8 @@ import mage.abilities.keyword.CumulativeUpkeepAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
/**
*
@ -28,7 +27,8 @@ public final class JujuBubble extends CardImpl {
this.addAbility(new CumulativeUpkeepAbility(new ManaCostsImpl<>("{1}")));
// When you play a card, sacrifice Juju Bubble.
this.addAbility(new JujuBubbleTriggeredAbility());
this.addAbility(new PlayCardTriggeredAbility(TargetController.YOU, Zone.BATTLEFIELD,
new SacrificeSourceEffect(), false));
// {2}: You gain 1 life.
this.addAbility(new SimpleActivatedAbility(new GainLifeEffect(1), new GenericManaCost(1)));
@ -42,35 +42,4 @@ public final class JujuBubble extends CardImpl {
public JujuBubble copy() {
return new JujuBubble(this);
}
}
class JujuBubbleTriggeredAbility extends TriggeredAbilityImpl {
JujuBubbleTriggeredAbility() {
super(Zone.BATTLEFIELD, new SacrificeSourceEffect(), false);
}
JujuBubbleTriggeredAbility(final JujuBubbleTriggeredAbility ability) {
super(ability);
}
@Override
public JujuBubbleTriggeredAbility copy() {
return new JujuBubbleTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return event.getPlayerId().equals(this.getControllerId());
}
@Override
public String getRule() {
return "When you play a card, sacrifice {this}";
}
}
}

View file

@ -89,7 +89,7 @@ class MizzixsMasteryOverloadEffect extends OneShotEffect {
while (controller.canRespond()
&& continueCasting
&& !copiedCards.isEmpty()) {
TargetCard targetCard = new TargetCard(0, 1, Zone.OUTSIDE,
TargetCard targetCard = new TargetCard(0, 1, Zone.EXILED,
new FilterCard("copied card to cast without paying its mana cost?"));
targetCard.setNotTarget(true);
if (controller.chooseTarget(Outcome.PlayForFree, copiedCards, targetCard, source, game)) {

View file

@ -68,8 +68,6 @@ class MnemonicDelugeEffect extends OneShotEffect {
Cards cards = new CardsImpl();
for (int i = 0; i < 3; i++) {
Card copiedCard = game.copyCard(card, source, source.getControllerId());
game.getExile().add(source.getSourceId(), "", copiedCard);
game.getState().setZone(copiedCard.getId(), Zone.EXILED);
cards.add(copiedCard);
}
for (Card copiedCard : cards.getCards(game)) {

View file

@ -2,7 +2,7 @@
package mage.cards.n;
import java.util.UUID;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.PlayCardTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
@ -15,9 +15,6 @@ import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
/**
*
@ -32,7 +29,8 @@ public final class NullProfusion extends CardImpl {
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new SkipDrawStepEffect()));
// Whenever you play a card, draw a card.
this.addAbility(new NullProfusionTriggeredAbility());
this.addAbility(new PlayCardTriggeredAbility(TargetController.YOU, Zone.BATTLEFIELD,
new DrawCardSourceControllerEffect(1)));
// Your maximum hand size is two.
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD,
@ -53,35 +51,4 @@ public final class NullProfusion extends CardImpl {
public NullProfusion copy() {
return new NullProfusion(this);
}
}
class NullProfusionTriggeredAbility extends TriggeredAbilityImpl {
NullProfusionTriggeredAbility() {
super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false);
}
NullProfusionTriggeredAbility(final NullProfusionTriggeredAbility ability) {
super(ability);
}
@Override
public NullProfusionTriggeredAbility copy() {
return new NullProfusionTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return event.getPlayerId().equals(this.getControllerId());
}
@Override
public String getRule() {
return "Whenever you play a card, draw a card.";
}
}
}

View file

@ -3,15 +3,18 @@ package mage.cards.p;
import mage.MageInt;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.BeginningOfEndStepTriggeredAbility;
import mage.abilities.common.PlayCardTriggeredAbility;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.ExileTopXMayPlayUntilEndOfTurnEffect;
import mage.abilities.keyword.DeathtouchAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.token.TreasureToken;
import mage.game.stack.Spell;
import java.util.UUID;
@ -53,10 +56,10 @@ public final class ProsperTomeBound extends CardImpl {
}
}
class ProsperTomeBoundTriggeredAbility extends TriggeredAbilityImpl {
class ProsperTomeBoundTriggeredAbility extends PlayCardTriggeredAbility {
ProsperTomeBoundTriggeredAbility() {
super(Zone.BATTLEFIELD, new CreateTokenEffect(new TreasureToken()));
super(TargetController.YOU, Zone.BATTLEFIELD, new CreateTokenEffect(new TreasureToken()));
this.flavorWord = "Pact Boon";
setTriggerPhrase("Whenever you play a card from exile, ");
}
@ -65,15 +68,9 @@ class ProsperTomeBoundTriggeredAbility extends TriggeredAbilityImpl {
super(ability);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SPELL_CAST
|| event.getType() == GameEvent.EventType.LAND_PLAYED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return isControlledBy(event.getPlayerId()) && event.getZone() == Zone.EXILED;
return super.checkTrigger(event, game) && event.getZone() == Zone.EXILED;
}
@Override

View file

@ -53,4 +53,4 @@ public final class PsionicRitual extends CardImpl {
public PsionicRitual copy() {
return new PsionicRitual(this);
}
}
}

View file

@ -2,7 +2,7 @@
package mage.cards.r;
import java.util.UUID;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.PlayCardTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.SkipDrawStepEffect;
@ -12,10 +12,8 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Duration;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.events.GameEvent.EventType;
/**
*
@ -30,7 +28,8 @@ public final class Recycle extends CardImpl {
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new SkipDrawStepEffect()));
// Whenever you play a card, draw a card.
this.addAbility(new RecycleTriggeredAbility());
this.addAbility(new PlayCardTriggeredAbility(TargetController.YOU, Zone.BATTLEFIELD,
new DrawCardSourceControllerEffect(1)));
// Your maximum hand size is two.
this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new MaximumHandSizeControllerEffect(2, Duration.WhileOnBattlefield, HandSizeModification.SET)));
@ -44,35 +43,4 @@ public final class Recycle extends CardImpl {
public Recycle copy() {
return new Recycle(this);
}
}
class RecycleTriggeredAbility extends TriggeredAbilityImpl {
RecycleTriggeredAbility() {
super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false);
}
RecycleTriggeredAbility(final RecycleTriggeredAbility ability) {
super(ability);
}
@Override
public RecycleTriggeredAbility copy() {
return new RecycleTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
return event.getPlayerId().equals(this.getControllerId());
}
@Override
public String getRule() {
return "Whenever you play a card, draw a card.";
}
}
}

View file

@ -1,14 +1,15 @@
package mage.cards.s;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.PlayCardTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.NamePredicate;
@ -82,10 +83,10 @@ class SearchTheCityExileEffect extends OneShotEffect {
}
}
class SearchTheCityTriggeredAbility extends TriggeredAbilityImpl {
class SearchTheCityTriggeredAbility extends PlayCardTriggeredAbility {
public SearchTheCityTriggeredAbility() {
super(Zone.BATTLEFIELD, new SearchTheCityExiledCardToHandEffect(), true);
super(TargetController.YOU, Zone.BATTLEFIELD, new SearchTheCityExiledCardToHandEffect(), true);
setTriggerPhrase("Whenever you play a card with the same name as one of the exiled cards, " );
}
@ -93,14 +94,9 @@ class SearchTheCityTriggeredAbility extends TriggeredAbilityImpl {
super(ability);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!event.getPlayerId().equals(this.getControllerId())) {
if (!super.checkTrigger(event, game)) {
return false;
}
String cardName = "";

View file

@ -175,8 +175,6 @@ class SpellbinderCopyEffect extends OneShotEffect {
if (controller.chooseUse(outcome, "Create a copy of " + imprintedInstant.getName() + '?', source, game)) {
Card copiedCard = game.copyCard(imprintedInstant, source, source.getControllerId());
if (copiedCard != null) {
game.getExile().add(source.getSourceId(), "", copiedCard);
game.getState().setZone(copiedCard.getId(), Zone.EXILED);
if (controller.chooseUse(outcome, "Cast the copied card without paying mana cost?", source, game)) {
if (copiedCard.getSpellAbility() != null) {
game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE);

View file

@ -90,8 +90,6 @@ class SpellweaverVoluteEffect extends OneShotEffect {
&& controller.chooseUse(Outcome.Copy, "Create a copy of " + enchantedCard.getName() + '?', source, game)) {
Card copiedCard = game.copyCard(enchantedCard, source, source.getControllerId());
if (copiedCard != null) {
controller.getGraveyard().add(copiedCard);
game.getState().setZone(copiedCard.getId(), Zone.GRAVEYARD);
if (controller.chooseUse(Outcome.PlayForFree, "Cast the copied card without paying mana cost?", source, game)) {
if (copiedCard.getSpellAbility() != null) {
game.getState().setValue("PlayFromNotOwnHandZone" + copiedCard.getId(), Boolean.TRUE);

View file

@ -144,7 +144,6 @@ class SurgeToVictoryCastEffect extends OneShotEffect {
if (copiedCard == null) {
return false;
}
player.moveCards(copiedCard, Zone.EXILED, source, game);
if (!player.chooseUse(outcome, "Cast the copy of the exiled card?", source, game)) {
return false;
}

View file

@ -102,7 +102,6 @@ class WildfireDevilsEffect extends OneShotEffect {
if (copiedCard == null) {
return false;
}
randomPlayer.moveCards(copiedCard, Zone.EXILED, source, game);
if (!controller.chooseUse(outcome, "Cast the copy of the exiled card?", source, game)) {
return false;
}

View file

@ -131,8 +131,6 @@ class ZethiArcaneBlademasterCastEffect extends OneShotEffect {
Cards copies = new CardsImpl();
for (Card card : cards.getCards(game)) {
Card copiedCard = game.copyCard(card, source, source.getControllerId());
game.getExile().add(source.getSourceId(), "", copiedCard);
game.getState().setZone(copiedCard.getId(), Zone.EXILED);
copies.add(copiedCard);
}
// simple way to choose the spells to cast; if you have a better tech, implement it!

View file

@ -0,0 +1,41 @@
package org.mage.test.cards.single.afc;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import mage.constants.PhaseStep;
import mage.constants.Zone;
public class ProsperTomeBoundTest extends CardTestPlayerBase {
static final String prosper = "Prosper, Tome-Bound";
@Test
// Author: alexander-novo
// As copies of cards aren't themselves cards, and Prosper specifies that his ability only triggers when a *card* is played from exile,
// Prosper shouldn't work with cards a la Mizzix's Mastery, which creates copies of cards in exile and then casts them.
// As of right now, this is true, but I'd guess this is due to the fact that effects like Mizzix's Mastery currently don't cast the copies from exile properly,
// not because Prosper is actually checking that the spells come from actual cards.
public void castCopyFromExileTest() {
String mastery = "Mizzix's Mastery";
String bolt = "Lightning Bolt";
// Cast mastery from hand targetting bolt, which will be exiled, copied, and cast. Prosper will see this cast.
addCard(Zone.GRAVEYARD, playerA, bolt);
addCard(Zone.HAND, playerA, mastery);
addCard(Zone.BATTLEFIELD, playerA, prosper);
// Enough mana for Mizzix's mastery
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
// Cast mastery. Choose target for bolt
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mastery, bolt);
addTarget(playerA, playerB);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertLife(playerB, 20 - 3);
assertExileCount(playerA, bolt, 1);
assertTokenCount(playerA, "Treasure Token", 0);
}
}

View file

@ -0,0 +1,38 @@
package org.mage.test.cards.single.c15;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import mage.constants.PhaseStep;
import mage.constants.Zone;
public class MizzixsMasteryTest extends CardTestPlayerBase {
private static final String mastery = "Mizzix's Mastery";
@Test
// Author: alexander-novo
// Making sure overload works correctly.
public void overloadTest() {
String fireball = "Delayed Blast Fireball";
// Prep for exiling fireball from graveyard, copying, and then casting copy
addCard(Zone.GRAVEYARD, playerA, fireball, 2);
addCard(Zone.HAND, playerA, mastery);
// Enough mana to overload mastery
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8);
// Cast Mizzix's Mastery targetting delayed blast fireball. This should exile it, copy it into exile, and then cast the copy from exile, which should end up dealing 5 damage to player B
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mastery + " with overload");
addTarget(playerA, fireball, 2);
setChoice(playerA, true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertLife(playerB, 20 - 2 * 5);
assertExileCount(playerA, fireball, 2);
}
}

View file

@ -0,0 +1,37 @@
package org.mage.test.cards.single.clb;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
import mage.constants.PhaseStep;
import mage.constants.Zone;
public class DelayedBlastFireballTest extends CardTestPlayerBase {
static final String fireball = "Delayed Blast Fireball";
@Test
// Author: alexander-novo
// Issue: magefree/mage#10435
// If you create a copy of a card in exile and then cast that copy, it should be cast from exile.
// But if we do this with Delayed Blast Fireball (and Mizzix's Mastery, for instance), it won't deal the extra damage for casting from exile.
public void testCopyCardAndCastFromExile() {
String mastery = "Mizzix's Mastery";
// Prep for exiling fireball from graveyard, copying, and then casting copy
addCard(Zone.GRAVEYARD, playerA, fireball);
addCard(Zone.HAND, playerA, mastery);
// Enough mana to cast mastery
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4);
// Cast Mizzix's Mastery targetting delayed blast fireball. This hsoul exile it, copy it into exile, and then cast the copy from exile, which should end up dealing 5 damage to player B
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mastery, fireball);
setChoice(playerA, true);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertLife(playerB, 20 - 5);
}
}

View file

@ -0,0 +1,105 @@
package mage.abilities.common;
import mage.abilities.TriggeredAbility;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect;
import mage.constants.TargetController;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
// Author: alexander-novo
// A triggered ability for cards which say "whenever <someone> play(s) a card..."
public class PlayCardTriggeredAbility extends TriggeredAbilityImpl {
private final TargetController targetController;
/**
*
* @param targetController Which player(s) playing cards can trigger this ability. Only [ANY, NOT_YOU, OPPONENT, YOU] are supported.
* @param zone
* @param effect
*/
public PlayCardTriggeredAbility(TargetController targetController, Zone zone, Effect effect) {
super(zone, effect);
this.targetController = targetController;
constructTriggerPhrase();
}
/**
*
* @param targetController Which player(s) playing cards can trigger this ability. Only [ANY, NOT_YOU, OPPONENT, YOU] are supported.
* @param zone
* @param effect
* @param optional
*/
public PlayCardTriggeredAbility(TargetController targetController, Zone zone, Effect effect, boolean optional) {
super(zone, effect, optional);
this.targetController = targetController;
constructTriggerPhrase();
}
private void constructTriggerPhrase() {
switch (targetController) {
case ANY:
setTriggerPhrase("Whenever a player plays play a card, ");
break;
case NOT_YOU:
setTriggerPhrase("Whenever another player plays a card, ");
break;
case OPPONENT:
setTriggerPhrase("Whenever an opponent plays a card, ");
break;
case YOU:
setTriggerPhrase("Whenever you play a card, ");
break;
default:
throw new UnsupportedOperationException("TargetController not supported");
}
}
public PlayCardTriggeredAbility(final PlayCardTriggeredAbility ability) {
super(ability);
this.targetController = ability.targetController;
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.SPELL_CAST
|| event.getType() == GameEvent.EventType.LAND_PLAYED;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
boolean playerMatches;
switch (targetController) {
case ANY:
playerMatches = true;
break;
case NOT_YOU:
playerMatches = !isControlledBy(event.getPlayerId());
break;
case OPPONENT:
playerMatches = game.getPlayer(getControllerId()).hasOpponent(event.getPlayerId(), game);
break;
case YOU:
playerMatches = isControlledBy(event.getPlayerId());
break;
default:
throw new UnsupportedOperationException("TargetController not supported");
}
// Make sure that, if a spell was cast, it came from an actual card (and not a copy of a card)
return playerMatches && (event.getType() != GameEvent.EventType.SPELL_CAST
|| !game.getSpell(event.getTargetId()).getCard().isCopy());
}
@Override
public TriggeredAbility copy() {
return new PlayCardTriggeredAbility(this);
}
}

View file

@ -445,89 +445,94 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
public boolean removeFromZone(Game game, Zone fromZone, Ability source) {
boolean removed = false;
MageObject lkiObject = null;
switch (fromZone) {
case GRAVEYARD:
removed = game.getPlayer(ownerId).removeFromGraveyard(this, game);
break;
case HAND:
removed = game.getPlayer(ownerId).removeFromHand(this, game);
break;
case LIBRARY:
removed = game.getPlayer(ownerId).removeFromLibrary(this, game);
break;
case EXILED:
if (game.getExile().getCard(getId(), game) != null) {
removed = game.getExile().removeCard(this, game);
}
break;
case STACK:
StackObject stackObject;
if (getSpellAbility() != null) {
stackObject = game.getStack().getSpell(getSpellAbility().getId(), false);
} else {
stackObject = game.getStack().getSpell(this.getId(), false);
}
if (isCopy()) { // copied cards have no need to be removed from a previous zone
removed = true;
} else {
switch (fromZone) {
case GRAVEYARD:
removed = game.getPlayer(ownerId).removeFromGraveyard(this, game);
break;
case HAND:
removed = game.getPlayer(ownerId).removeFromHand(this, game);
break;
case LIBRARY:
removed = game.getPlayer(ownerId).removeFromLibrary(this, game);
break;
case EXILED:
if (game.getExile().getCard(getId(), game) != null) {
removed = game.getExile().removeCard(this, game);
}
break;
case STACK:
StackObject stackObject;
if (getSpellAbility() != null) {
stackObject = game.getStack().getSpell(getSpellAbility().getId(), false);
} else {
stackObject = game.getStack().getSpell(this.getId(), false);
}
// handle half of Split Cards on stack
if (stackObject == null && (this instanceof SplitCard)) {
stackObject = game.getStack().getSpell(((SplitCard) this).getLeftHalfCard().getId(), false);
if (stackObject == null) {
stackObject = game.getStack().getSpell(((SplitCard) this).getRightHalfCard().getId(),
false);
}
}
// handle half of Modal Double Faces Cards on stack
if (stackObject == null && (this instanceof ModalDoubleFacedCard)) {
stackObject = game.getStack().getSpell(((ModalDoubleFacedCard) this).getLeftHalfCard().getId(),
false);
if (stackObject == null) {
stackObject = game.getStack()
.getSpell(((ModalDoubleFacedCard) this).getRightHalfCard().getId(), false);
}
}
if (stackObject == null && (this instanceof AdventureCard)) {
stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false);
}
// handle half of Split Cards on stack
if (stackObject == null && (this instanceof SplitCard)) {
stackObject = game.getStack().getSpell(((SplitCard) this).getLeftHalfCard().getId(), false);
if (stackObject == null) {
stackObject = game.getStack().getSpell(((SplitCard) this).getRightHalfCard().getId(), false);
stackObject = game.getStack().getSpell(getId(), false);
}
}
// handle half of Modal Double Faces Cards on stack
if (stackObject == null && (this instanceof ModalDoubleFacedCard)) {
stackObject = game.getStack().getSpell(((ModalDoubleFacedCard) this).getLeftHalfCard().getId(), false);
if (stackObject == null) {
stackObject = game.getStack().getSpell(((ModalDoubleFacedCard) this).getRightHalfCard().getId(), false);
if (stackObject != null) {
removed = game.getStack().remove(stackObject, game);
lkiObject = stackObject;
}
}
if (stackObject == null && (this instanceof AdventureCard)) {
stackObject = game.getStack().getSpell(((AdventureCard) this).getSpellCard().getId(), false);
}
if (stackObject == null) {
stackObject = game.getStack().getSpell(getId(), false);
}
if (stackObject != null) {
removed = game.getStack().remove(stackObject, game);
lkiObject = stackObject;
}
break;
case COMMAND:
for (CommandObject commandObject : game.getState().getCommand()) {
if (commandObject.getId().equals(objectId)) {
lkiObject = commandObject;
break;
case COMMAND:
for (CommandObject commandObject : game.getState().getCommand()) {
if (commandObject.getId().equals(objectId)) {
lkiObject = commandObject;
}
}
}
if (lkiObject != null) {
removed = game.getState().getCommand().remove(lkiObject);
}
break;
case OUTSIDE:
if (isCopy()) { // copied cards have no need to be removed from a previous zone
if (lkiObject != null) {
removed = game.getState().getCommand().remove(lkiObject);
}
break;
case OUTSIDE:
if (game.getPlayer(ownerId).getSideboard().contains(this.getId())) {
game.getPlayer(ownerId).getSideboard().remove(this.getId());
removed = true;
} else if (game.getPhase() == null) {
// E.g. Commander of commander game
removed = true;
} else {
// Unstable - Summon the Pack
removed = true;
}
break;
case BATTLEFIELD: // for sacrificing permanents or putting to library
removed = true;
} else if (game.getPlayer(ownerId).getSideboard().contains(this.getId())) {
game.getPlayer(ownerId).getSideboard().remove(this.getId());
removed = true;
} else if (game.getPhase() == null) {
// E.g. Commander of commander game
removed = true;
} else {
// Unstable - Summon the Pack
removed = true;
}
break;
case BATTLEFIELD: // for sacrificing permanents or putting to library
removed = true;
break;
default:
MageObject sourceObject = game.getObject(source);
logger.fatal("Invalid from zone [" + fromZone + "] for card [" + this.getIdName()
+ "] source [" + (sourceObject != null ? sourceObject.getName() : "null") + ']');
break;
break;
default:
MageObject sourceObject = game.getObject(source);
logger.fatal("Invalid from zone [" + fromZone + "] for card [" + this.getIdName()
+ "] source [" + (sourceObject != null ? sourceObject.getName() : "null") + ']');
break;
}
}
if (removed) {
if (fromZone != Zone.OUTSIDE) {

View file

@ -1388,10 +1388,16 @@ public class GameState implements Serializable, Copyable<GameState> {
// main part prepare (must be called after other parts cause it change ids for all)
prepareCardForCopy(mainCardToCopy, copiedCard, newController);
// 707.12. An effect that instructs a player to cast a copy of an object (and not just copy a spell) follows the rules for casting spells, except that the copy is created in the same zone the object is in and then cast while another spell or ability is resolving.
Zone copyToZone = game.getState().getZone(mainCardToCopy.getId());
if (copyToZone == Zone.BATTLEFIELD) {
throw new UnsupportedOperationException("Cards cannot be copied while on the Battlefield");
}
// add all parts to the game
copiedParts.forEach(card -> {
copiedCards.put(card.getId(), card);
addCard(card);
addCard(card, copyToZone);
});
// copied cards removes from game after battlefield/stack leaves, so remember it here as workaround to fix freeze, see https://github.com/magefree/mage/issues/5437