[ZNR] Improved modal double faces cards implementation and more tests (#7012)

This commit is contained in:
Oleg Agafonov 2020-10-27 22:07:34 +04:00
parent 19cd742f40
commit e4c8ba046a
3 changed files with 163 additions and 21 deletions

View file

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

View file

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

View file

@ -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<UUID> 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<UUID> 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<CardType> 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 isnt 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<SuperType> getSuperType() {
return EnumSet.noneOf(SuperType.class);
}
@Override
public Abilities<Ability> getAbilities() {
Abilities<Ability> 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<Ability> 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<String> 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<ManaCost> 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();
}
}