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

@ -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