mirror of
https://github.com/correl/mage.git
synced 2025-01-11 19:13:02 +00:00
[ZNR] Improved modal double faces cards implementation and more tests (#7012)
This commit is contained in:
parent
19cd742f40
commit
e4c8ba046a
3 changed files with 163 additions and 21 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 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<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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue