diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaces/ModalDoubleFacesCardsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaces/ModalDoubleFacesCardsTest.java index 27fd8fd1db7..9cb91a2588d 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaces/ModalDoubleFacesCardsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/modaldoublefaces/ModalDoubleFacesCardsTest.java @@ -1,7 +1,10 @@ package org.mage.test.cards.cost.modaldoublefaces; +import mage.cards.Card; import mage.constants.PhaseStep; +import mage.constants.SubType; import mage.constants.Zone; +import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -160,6 +163,78 @@ public class ModalDoubleFacesCardsTest extends CardTestPlayerBase { assertAllCommandsUsed(); } + @Test + public void test_Characteristics() { + // rules: + // While a double-faced card isn’t on the stack or battlefield, consider only the characteristics + // of its front face. For example, the above card has only the characteristics of Sejiri Shelter + // in the graveyard, even if it was Sejiri Glacier on the battlefield before it was put into the + // graveyard. Notably, this means that Sejiri Shelter is a nonland card even though you could play + // it as a land + removeAllCardsFromHand(playerA); + removeAllCardsFromLibrary(playerA); + + // Akoum Warrior {5}{R} - creature + // Akoum Teeth - land + addCard(Zone.HAND, playerA, "Akoum Warrior"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + // stats in hand + Assert.assertEquals(1, getHandCards(playerA).size()); + Card card = getHandCards(playerA).get(0); + Assert.assertFalse("must be non land", card.isLand()); + Assert.assertTrue("must be creature", card.isCreature()); + Assert.assertTrue("must be minotaur", card.getSubtype(currentGame).contains(SubType.MINOTAUR)); + Assert.assertEquals("power", 4, card.getPower().getValue()); + Assert.assertEquals("toughness", 5, card.getToughness().getValue()); + } + + @Test + public void test_PlayFromNonHand_GraveyardByFlashback() { + removeAllCardsFromHand(playerA); + removeAllCardsFromLibrary(playerA); + + // Emeria's Call - Sorcery {4}{W}{W}{W} + // Emeria, Shattered Skyclave - land + // Create two 4/4 white Angel Warrior creature tokens with flying. Non-Angel creatures you control gain indestructible until your next turn. + addCard(Zone.GRAVEYARD, playerA, "Emeria's Call"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 7); + // + // When Snapcaster Mage enters the battlefield, target instant or sorcery card in your graveyard gains flashback + // until end of turn. The flashback cost is equal to its mana cost. + addCard(Zone.HAND, playerA, "Snapcaster Mage"); // {1}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + checkGraveyardCount("grave before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Emeria's Call", 1); + checkPlayableAbility("can't play as sorcery", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Emeria's Call", false); + checkPlayableAbility("can't play as land", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Emeria, Shattered Skyclave", false); + + // cast Snapcaster and give flashback + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Snapcaster Mage"); + addTarget(playerA, "Emeria's Call"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkGraveyardCount("grave before cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Emeria's Call", 1); + checkPlayableAbility("can play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback", true); + + // cast as sorcery with flashback + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Flashback"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkExileCount("exile after", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Emeria's Call", 1); + checkPermanentCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Emeria's Call", 0); + checkPermanentCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Emeria, Shattered Skyclave", 0); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Snapcaster Mage", 1); + } + @Test public void test_Single_MalakirRebirth() { // Malakir Rebirth diff --git a/Mage/src/main/java/mage/cards/CardImpl.java b/Mage/src/main/java/mage/cards/CardImpl.java index b0b3e4fce76..ecd7c163eb8 100644 --- a/Mage/src/main/java/mage/cards/CardImpl.java +++ b/Mage/src/main/java/mage/cards/CardImpl.java @@ -309,7 +309,8 @@ public abstract class CardImpl extends MageObjectImpl implements Card { // workaround to add dynamic flashback ability from main card to all parts (example: Snapcaster Mage gives flashback to split card) if (!this.getId().equals(this.getMainCard().getId())) { CardState mainCardState = game.getState().getCardState(this.getMainCard().getId()); - if (mainCardState != null + if (this.getSpellAbility() != null // lands can't be casted (haven't spell ability), so ignore it + && mainCardState != null && !mainCardState.hasLostAllAbilities() && mainCardState.getAbilities().containsClass(FlashbackAbility.class)) { FlashbackAbility flash = new FlashbackAbility(this.getManaCost(), this.isInstant() ? TimingRule.INSTANT : TimingRule.SORCERY); diff --git a/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java b/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java index 7b54ec2cdf5..9db3fb9dce6 100644 --- a/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java +++ b/Mage/src/main/java/mage/cards/ModalDoubleFacesCard.java @@ -1,18 +1,21 @@ package mage.cards; +import mage.MageInt; import mage.MageObject; +import mage.ObjectColor; import mage.abilities.Abilities; import mage.abilities.AbilitiesImpl; import mage.abilities.Ability; import mage.abilities.SpellAbility; -import mage.constants.CardType; -import mage.constants.SpellAbilityType; -import mage.constants.SubType; -import mage.constants.Zone; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.constants.*; import mage.game.Game; import mage.game.events.ZoneChangeEvent; +import mage.util.SubTypeList; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.UUID; @@ -68,8 +71,8 @@ public abstract class ModalDoubleFacesCard extends CardImpl { @Override public boolean moveToZone(Zone toZone, UUID sourceId, Game game, boolean flag, List appliedEffects) { if (super.moveToZone(toZone, sourceId, game, flag, appliedEffects)) { - game.getState().setZone(getLeftHalfCard().getId(), toZone); - game.getState().setZone(getRightHalfCard().getId(), toZone); + game.getState().setZone(leftHalfCard.getId(), toZone); + game.getState().setZone(rightHalfCard.getId(), toZone); return true; } return false; @@ -78,16 +81,16 @@ public abstract class ModalDoubleFacesCard extends CardImpl { @Override public void setZone(Zone zone, Game game) { super.setZone(zone, game); - game.setZone(getLeftHalfCard().getId(), zone); - game.setZone(getRightHalfCard().getId(), zone); + game.setZone(leftHalfCard.getId(), zone); + game.setZone(rightHalfCard.getId(), zone); } @Override public boolean moveToExile(UUID exileId, String name, UUID sourceId, Game game, List appliedEffects) { if (super.moveToExile(exileId, name, sourceId, game, appliedEffects)) { Zone currentZone = game.getState().getZone(getId()); - game.getState().setZone(getLeftHalfCard().getId(), currentZone); - game.getState().setZone(getRightHalfCard().getId(), currentZone); + game.getState().setZone(leftHalfCard.getId(), currentZone); + game.getState().setZone(rightHalfCard.getId(), currentZone); return true; } return false; @@ -106,27 +109,64 @@ public abstract class ModalDoubleFacesCard extends CardImpl { return; } super.updateZoneChangeCounter(game, event); - getLeftHalfCard().updateZoneChangeCounter(game, event); - getRightHalfCard().updateZoneChangeCounter(game, event); + leftHalfCard.updateZoneChangeCounter(game, event); + rightHalfCard.updateZoneChangeCounter(game, event); } @Override public boolean cast(Game game, Zone fromZone, SpellAbility ability, UUID controllerId) { switch (ability.getSpellAbilityType()) { case MODAL_LEFT: - return this.getLeftHalfCard().cast(game, fromZone, ability, controllerId); + return this.leftHalfCard.cast(game, fromZone, ability, controllerId); case MODAL_RIGHT: - return this.getRightHalfCard().cast(game, fromZone, ability, controllerId); + return this.rightHalfCard.cast(game, fromZone, ability, controllerId); default: - this.getLeftHalfCard().getSpellAbility().setControllerId(controllerId); - this.getRightHalfCard().getSpellAbility().setControllerId(controllerId); + if (this.leftHalfCard.getSpellAbility() != null) + this.leftHalfCard.getSpellAbility().setControllerId(controllerId); + if (this.rightHalfCard.getSpellAbility() != null) + this.rightHalfCard.getSpellAbility().setControllerId(controllerId); return super.cast(game, fromZone, ability, controllerId); } } + + @Override + public ArrayList getCardType() { + // CardImpl's constructor can call some code on init, so you must check left/right before + // it's a bad workaround + return leftHalfCard != null ? leftHalfCard.getCardType() : cardType; + } + + @Override + public SubTypeList getSubtype(Game game) { + // rules: While a double-faced card isn’t on the stack or battlefield, consider only the characteristics of its front face. + + // CardImpl's constructor can call some code on init, so you must check left/right before + return leftHalfCard != null ? leftHalfCard.getSubtype(game) : subtype; + } + + @Override + public boolean hasSubtype(SubType subtype, Game game) { + return leftHalfCard.hasSubtype(subtype, game); + } + + @Override + public EnumSet getSuperType() { + return EnumSet.noneOf(SuperType.class); + } + @Override public Abilities getAbilities() { Abilities allAbilites = new AbilitiesImpl<>(); + + // ignore default spell ability from main card (only halfes are actual) + for (Ability ability : super.getAbilities()) { + if (ability instanceof SpellAbility && ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.MODAL) { + continue; + } + allAbilites.add(ability); + } + allAbilites.addAll(super.getAbilities()); allAbilites.addAll(leftHalfCard.getAbilities()); allAbilites.addAll(rightHalfCard.getAbilities()); @@ -134,7 +174,8 @@ public abstract class ModalDoubleFacesCard extends CardImpl { } public Abilities getSharedAbilities(Game game) { - return super.getAbilities(game); + // no shared abilities for mdf cards (e.g. must be left or right only) + return new AbilitiesImpl<>(); } @Override @@ -155,8 +196,18 @@ public abstract class ModalDoubleFacesCard extends CardImpl { } @Override - public List getRules() { - return new ArrayList<>(); + public boolean hasAbility(Ability ability, Game game) { + return super.hasAbility(ability, game); + } + + @Override + public ObjectColor getColor(Game game) { + return leftHalfCard.getColor(game); + } + + @Override + public ObjectColor getFrameColor(Game game) { + return leftHalfCard.getFrameColor(game); } @Override @@ -169,6 +220,11 @@ public abstract class ModalDoubleFacesCard extends CardImpl { rightHalfCard.setOwnerId(ownerId); } + @Override + public ManaCosts getManaCost() { + return leftHalfCard.getManaCost(); + } + @Override public int getConvertedManaCost() { // Rules: @@ -178,6 +234,16 @@ public abstract class ModalDoubleFacesCard extends CardImpl { // mana cost of a transforming double-faced card is determined. // on stack or battlefield it must be half card with own cost - return getLeftHalfCard().getConvertedManaCost(); + return leftHalfCard.getConvertedManaCost(); + } + + @Override + public MageInt getPower() { + return leftHalfCard.getPower(); + } + + @Override + public MageInt getToughness() { + return leftHalfCard.getToughness(); } }