implement [MH3] Grist, Voracious Larva // Grist, the Plague Swarm

This commit is contained in:
Susucre 2024-05-23 00:17:01 +02:00
parent 79165f269c
commit a1bbb49a38
4 changed files with 595 additions and 0 deletions

View file

@ -0,0 +1,163 @@
package mage.cards.g;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.LoyaltyAbility;
import mage.abilities.effects.Effect;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenCopyTargetEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DestroyTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.Cards;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.game.permanent.token.IzoniInsectToken;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.targetpointer.FixedTargets;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author Susucr
*/
public final class GristThePlagueSwarm extends CardImpl {
public GristThePlagueSwarm(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.GRIST);
this.setStartingLoyalty(3);
this.color.setBlack(true);
this.color.setGreen(true);
this.nightCard = true;
// +1: Create a 1/1 black and green Insect creature token, then mill two cards. Put a deathtouch counter on the token if a black card was milled this way.
this.addAbility(new LoyaltyAbility(new GristThePlagueSwarmPlus1Effect(), 1));
// -2: Destroy target artifact or enchantment.
Ability ability = new LoyaltyAbility(new DestroyTargetEffect(), -2);
ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_ARTIFACT_OR_ENCHANTMENT));
this.addAbility(ability);
// -6: For each creature card in your graveyard, create a token that's a copy of it, except it's a 1/1 black and green Insect.
this.addAbility(new LoyaltyAbility(new GristThePlagueSwarmMinus6Effect(), -6));
}
private GristThePlagueSwarm(final GristThePlagueSwarm card) {
super(card);
}
@Override
public GristThePlagueSwarm copy() {
return new GristThePlagueSwarm(this);
}
}
class GristThePlagueSwarmPlus1Effect extends OneShotEffect {
GristThePlagueSwarmPlus1Effect() {
super(Outcome.PutCreatureInPlay);
staticText = "Create a 1/1 black and green Insect creature token, then mill two cards. "
+ "Put a deathtouch counter on the token if a black card was milled this way.";
}
private GristThePlagueSwarmPlus1Effect(final GristThePlagueSwarmPlus1Effect effect) {
super(effect);
}
@Override
public GristThePlagueSwarmPlus1Effect copy() {
return new GristThePlagueSwarmPlus1Effect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
// Create a 1/1 black and green Insect creature token
CreateTokenEffect effect = new CreateTokenEffect(new IzoniInsectToken());
effect.apply(game, source);
// Then mill two cards
Cards cards = controller.millCards(2, source, game);
// Put a deathtouch counter on the token if a black card was milled this way.
if (cards.getCards(game).stream().anyMatch(card -> card.getColor(game).isBlack())) {
List<Permanent> tokens = effect
.getLastAddedTokenIds()
.stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!tokens.isEmpty()) {
Effect addEffect = new AddCountersTargetEffect(CounterType.DEATHTOUCH.createInstance());
addEffect.setTargetPointer(new FixedTargets(tokens, game));
addEffect.apply(game, source);
}
}
return true;
}
}
class GristThePlagueSwarmMinus6Effect extends OneShotEffect {
GristThePlagueSwarmMinus6Effect() {
super(Outcome.PutCreatureInPlay);
staticText = "For each creature card in your graveyard, create a token that's a copy of it, "
+ "except it's a 1/1 black and green Insect.";
}
private GristThePlagueSwarmMinus6Effect(final GristThePlagueSwarmMinus6Effect effect) {
super(effect);
}
@Override
public GristThePlagueSwarmMinus6Effect copy() {
return new GristThePlagueSwarmMinus6Effect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
if (controller == null) {
return false;
}
Set<Card> cards = controller.getGraveyard().getCards(StaticFilters.FILTER_CARD_CREATURE, game);
if (cards.isEmpty()) {
return false;
}
for (Card card : cards) {
CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect(
null, null, false, 1, false,
false, null, 1, 1, false
);
effect.setSavedPermanent(new PermanentCard(card, controller.getId(), game));
effect.setOnlyColor(new ObjectColor("BG"));
effect.setOnlySubType(SubType.INSECT);
effect.apply(game, source);
}
return true;
}
}

View file

@ -0,0 +1,103 @@
package mage.cards.g;
import mage.MageInt;
import mage.abilities.Pronoun;
import mage.abilities.common.EntersBattlefieldThisOrAnotherTriggeredAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.DoIfCostPaid;
import mage.abilities.effects.common.ExileAndReturnSourceEffect;
import mage.abilities.keyword.DeathtouchAbility;
import mage.abilities.keyword.TransformAbility;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.*;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.events.EntersTheBattlefieldEvent;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.game.stack.Spell;
import java.util.UUID;
/**
* @author Susucr
*/
public final class GristVoraciousLarva extends CardImpl {
public GristVoraciousLarva(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}");
this.supertype.add(SuperType.LEGENDARY);
this.subtype.add(SubType.INSECT);
this.power = new MageInt(1);
this.toughness = new MageInt(2);
this.secondSideCardClazz = GristThePlagueSwarm.class;
// Deathtouch
this.addAbility(DeathtouchAbility.getInstance());
// Whenever Grist, Voracious Larva or another creature enters the battlefield under your control, if it entered from your graveyard or was cast from your graveyard, you may pay {G}. If you do, exile Grist, then return it to the battlefield transformed under its owner's control.
this.addAbility(new TransformAbility());
this.addAbility(new GristVoraciousLarvaTriggeredAbility());
}
private GristVoraciousLarva(final GristVoraciousLarva card) {
super(card);
}
@Override
public GristVoraciousLarva copy() {
return new GristVoraciousLarva(this);
}
}
class GristVoraciousLarvaTriggeredAbility extends EntersBattlefieldThisOrAnotherTriggeredAbility {
GristVoraciousLarvaTriggeredAbility() {
super(
new DoIfCostPaid(
new ExileAndReturnSourceEffect(PutCards.BATTLEFIELD_TRANSFORMED, Pronoun.IT),
new ManaCostsImpl<>("{G}")
), StaticFilters.FILTER_PERMANENT_CREATURE, false, true);
setTriggerPhrase("Whenever {this} or another creature enters the battlefield under your control, "
+ "if it entered from your graveyard or was cast from your graveyard, ");
}
private GristVoraciousLarvaTriggeredAbility(final GristVoraciousLarvaTriggeredAbility ability) {
super(ability);
}
@Override
public GristVoraciousLarvaTriggeredAbility copy() {
return new GristVoraciousLarvaTriggeredAbility(this);
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
EntersTheBattlefieldEvent zEvent = (EntersTheBattlefieldEvent) event;
if (zEvent == null) {
return false;
}
Permanent permanent = zEvent.getTarget();
if (permanent == null) {
return false;
}
Zone fromZone = zEvent.getFromZone();
boolean fromGraveyard = false;
if (fromZone == Zone.GRAVEYARD) {
// Directly from the graveyard
fromGraveyard = true;
} else if (fromZone == Zone.STACK) {
// Get spell in the stack.
Spell spell = game.getSpellOrLKIStack(permanent.getId());
if (spell != null && spell.getFromZone() == Zone.GRAVEYARD) {
// Creature was cast from graveyard
fromGraveyard = true;
}
}
return fromGraveyard && super.checkTrigger(event, game);
}
}

View file

@ -46,6 +46,8 @@ public final class ModernHorizons3 extends ExpansionSet {
cards.add(new SetCardInfo("Forest", 308, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("Frogmyr Enforcer", 120, Rarity.UNCOMMON, mage.cards.f.FrogmyrEnforcer.class));
cards.add(new SetCardInfo("Grim Servant", 97, Rarity.UNCOMMON, mage.cards.g.GrimServant.class));
cards.add(new SetCardInfo("Grist, the Plague Swarm", 251, Rarity.MYTHIC, mage.cards.g.GristThePlagueSwarm.class));
cards.add(new SetCardInfo("Grist, Voracious Larva", 251, Rarity.MYTHIC, mage.cards.g.GristVoraciousLarva.class));
cards.add(new SetCardInfo("Island", 305, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS));
cards.add(new SetCardInfo("It That Heralds the End", 9, Rarity.UNCOMMON, mage.cards.i.ItThatHeraldsTheEnd.class));
cards.add(new SetCardInfo("Jet Medallion", 292, Rarity.RARE, mage.cards.j.JetMedallion.class));

View file

@ -0,0 +1,327 @@
package org.mage.test.cards.single.mh3;
import mage.ObjectColor;
import mage.constants.CardType;
import mage.constants.PhaseStep;
import mage.constants.SubType;
import mage.constants.Zone;
import mage.counters.CounterType;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author Susucr
*/
public class GristVoraciousLarvaTest extends CardTestPlayerBase {
/**
* {@link mage.cards.g.GristVoraciousLarva Grist, Voracious Larva} {G}
* Legendary Creature Insect
* Deathtouch
* Whenever Grist, Voracious Larva or another creature enters the battlefield under your control, if it entered from your graveyard or you cast it from your graveyard, you may pay {G}. If you do, exile Grist, then return it to the battlefield transformed under its owners control.
* 1/2
* // {@link mage.cards.g.GristThePlagueSwarm Grist, the Plague Swarm}
* Legendary Planeswalker Grist
* +1: Create a 1/1 black and green Insect creature token, then mill two cards. Put a deathtouch counter on the token if a black card was milled this way.
* 2: Destroy target artifact or enchantment.
* 6: For each creature card in your graveyard, create a token thats a copy of it, except its a 1/1 black and green Insect.
* Loyalty: 3
*/
private static final String grist = "Grist, Voracious Larva";
private static final String gristPW = "Grist, the Plague Swarm";
@Test
public void test_Unearth_Trigger_NoMana() {
setStrictChooseMode(true);
addCard(Zone.GRAVEYARD, playerA, grist);
addCard(Zone.BATTLEFIELD, playerA, "Swamp");
addCard(Zone.HAND, playerA, "Unearth");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unearth", grist);
setChoice(playerA, false); // can't pay for the trigger
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, grist, 1);
}
@Test
public void test_Unearth_Trigger_Pay() {
setStrictChooseMode(true);
addCard(Zone.GRAVEYARD, playerA, grist);
addCard(Zone.BATTLEFIELD, playerA, "Swamp");
addCard(Zone.BATTLEFIELD, playerA, "Forest");
addCard(Zone.HAND, playerA, "Unearth");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unearth", grist);
setChoice(playerA, true); // pay for the trigger
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, gristPW, 1);
}
@Test
public void test_Unearth_Trigger_NoPay() {
setStrictChooseMode(true);
addCard(Zone.GRAVEYARD, playerA, grist);
addCard(Zone.BATTLEFIELD, playerA, "Swamp");
addCard(Zone.BATTLEFIELD, playerA, "Forest");
addCard(Zone.HAND, playerA, "Unearth");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Unearth", grist);
setChoice(playerA, false); // don't pay for the trigger
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, grist, 1);
}
@Test
public void test_Bloodghast_Trigger() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, grist);
addCard(Zone.HAND, playerA, "Forest");
addCard(Zone.GRAVEYARD, playerA, "Bloodghast");
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Forest");
setChoice(playerA, true); // yes to Bloodghast's trigger
setChoice(playerA, true); // pay for Grist's trigger
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, gristPW, 1);
}
@Test
public void test_CastBloodghast_NoTrigger() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, grist);
addCard(Zone.BATTLEFIELD, playerA, "Bayou", 3);
addCard(Zone.HAND, playerA, "Bloodghast");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodghast");
// no Grist's trigger
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, grist, 1);
assertPermanentCount(playerA, "Bloodghast", 1);
}
@Test
public void test_CastGravecrawler_Trigger() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, grist);
addCard(Zone.BATTLEFIELD, playerA, "Bayou", 2);
addCard(Zone.BATTLEFIELD, playerA, "Gravecrawler");
addCard(Zone.GRAVEYARD, playerA, "Gravecrawler");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gravecrawler");
setChoice(playerA, true); // pay for Grist's trigger
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, gristPW, 1);
assertPermanentCount(playerA, "Gravecrawler", 2);
}
@Test
public void test_Cast_NonCreature_NoTrigger() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, grist);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Muldrotha, the Gravetide"); // During each of your turns, you may play a land and cast a permanent spell of each permanent type from your graveyard.
addCard(Zone.GRAVEYARD, playerA, "Mox Jet");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mox Jet");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, grist, 1);
assertPermanentCount(playerA, "Mox Jet", 1);
}
@Test
public void test_Play_DryadArbor_FromGraveyard_Trigger() {
setStrictChooseMode(true);
addCard(Zone.BATTLEFIELD, playerA, grist);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 1);
addCard(Zone.BATTLEFIELD, playerA, "Muldrotha, the Gravetide"); // During each of your turns, you may play a land and cast a permanent spell of each permanent type from your graveyard.
addCard(Zone.GRAVEYARD, playerA, "Dryad Arbor");
playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dryad Arbor");
setChoice(playerA, "Land"); // Choose to consume the Land part of Muldrotha, "Creature" would have not worked anyway since Dryad Arbor can not be cast.
setChoice(playerA, true); // pay for Grist's trigger
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, gristPW, 1);
assertPermanentCount(playerA, "Dryad Arbor", 1);
}
@Test
public void test_PlusOne_NoLibrary() {
setStrictChooseMode(true);
removeAllCardsFromLibrary(playerA);
addCard(Zone.BATTLEFIELD, playerA, gristPW);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, 0);
assertPermanentCount(playerA, gristPW, 1);
assertCounterCount(playerA, gristPW, CounterType.LOYALTY, 3 + 1);
assertPermanentCount(playerA, "Insect Token", 1);
assertCounterCount(playerA, "Insect Token", CounterType.DEATHTOUCH, 0);
}
@Test
public void test_PlusOne_MillNonBlack() {
setStrictChooseMode(true);
skipInitShuffling();
addCard(Zone.BATTLEFIELD, playerA, gristPW);
addCard(Zone.LIBRARY, playerA, "Grizzly Bears", 2);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, 2);
assertPermanentCount(playerA, gristPW, 1);
assertCounterCount(playerA, gristPW, CounterType.LOYALTY, 3 + 1);
assertPermanentCount(playerA, "Insect Token", 1);
assertCounterCount(playerA, "Insect Token", CounterType.DEATHTOUCH, 0);
}
@Test
public void test_PlusOne_MillBlack() {
setStrictChooseMode(true);
skipInitShuffling();
addCard(Zone.BATTLEFIELD, playerA, gristPW);
addCard(Zone.LIBRARY, playerA, "Blood Artist", 1);
addCard(Zone.LIBRARY, playerA, "Grizzly Bears", 1);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, 2);
assertPermanentCount(playerA, gristPW, 1);
assertCounterCount(playerA, gristPW, CounterType.LOYALTY, 3 + 1);
assertPermanentCount(playerA, "Insect Token", 1);
assertCounterCount(playerA, "Insect Token", CounterType.DEATHTOUCH, 1);
}
@Test
public void test_PlusOne_MillBlack_Chatterfang() {
setStrictChooseMode(true);
skipInitShuffling();
addCard(Zone.BATTLEFIELD, playerA, gristPW);
addCard(Zone.BATTLEFIELD, playerA, "Chatterfang, Squirrel General"); // If one or more tokens would be created under your control, those tokens plus that many 1/1 green Squirrel creature tokens are created instead.
addCard(Zone.LIBRARY, playerA, "Blood Artist", 1);
addCard(Zone.LIBRARY, playerA, "Grizzly Bears", 1);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, 2);
assertPermanentCount(playerA, gristPW, 1);
assertCounterCount(playerA, gristPW, CounterType.LOYALTY, 3 + 1);
assertPermanentCount(playerA, "Insect Token", 1);
assertCounterCount(playerA, "Insect Token", CounterType.DEATHTOUCH, 1);
assertPermanentCount(playerA, "Squirrel Token", 1);
assertCounterCount(playerA, "Squirrel Token", CounterType.DEATHTOUCH, 1);
}
@Test
public void test_Minus6() {
setStrictChooseMode(true);
skipInitShuffling();
addCard(Zone.BATTLEFIELD, playerA, gristPW);
addCard(Zone.GRAVEYARD, playerA, "Dryad Arbor", 1);
addCard(Zone.GRAVEYARD, playerA, "Bitterblossom", 1);
addCard(Zone.GRAVEYARD, playerA, "Taiga", 1);
addCard(Zone.GRAVEYARD, playerA, "Baneslayer Angel", 1);
addCard(Zone.GRAVEYARD, playerA, "Keranos, God of Storms", 1);
addCard(Zone.GRAVEYARD, playerA, "Grist, the Hunger Tide", 1);
addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, gristPW, CounterType.LOYALTY, 4);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-6");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, 6); // cards are still there
assertPermanentCount(playerA, 1 + 4);
assertCounterCount(playerA, gristPW, CounterType.LOYALTY, 1);
// None of those are creature in graveyard
assertPermanentCount(playerA, "Bitterblossom", 0);
assertPermanentCount(playerA, "Taiga", 0);
// All of those are creature in graveyard
assertPermanentCount(playerA, "Dryad Arbor", 1);
assertPermanentCount(playerA, "Baneslayer Angel", 1);
assertPermanentCount(playerA, "Keranos, God of Storms", 1);
assertPermanentCount(playerA, "Grist, the Hunger Tide", 1);
// Dryad Arbor is still a Land Forest, but no longer Dryad
assertPowerToughness(playerA, "Dryad Arbor", 1, 1);
assertType("Dryad Arbor", CardType.CREATURE, true);
assertType("Dryad Arbor", CardType.LAND, true);
assertSubtype("Dryad Arbor", SubType.INSECT);
assertNotSubtype("Dryad Arbor", SubType.DRYAD);
assertSubtype("Dryad Arbor", SubType.FOREST);
assertColor(playerA, "Dryad Arbor", "{G}{B}", true);
// Baneslayer Angel is now a Baneslayer Insect
assertPowerToughness(playerA, "Baneslayer Angel", 1, 1);
assertType("Baneslayer Angel", CardType.CREATURE, true);
assertSubtype("Baneslayer Angel", SubType.INSECT);
assertNotSubtype("Baneslayer Angel", SubType.ANGEL);
assertColor(playerA, "Baneslayer Angel", "{G}{B}", true);
assertColor(playerA, "Baneslayer Angel", ObjectColor.WHITE, false);
// Keranos token is just an Enchantment
assertType("Keranos, God of Storms", CardType.CREATURE, false);
assertType("Keranos, God of Storms", CardType.ENCHANTMENT, true);
assertNotSubtype("Keranos, God of Storms", SubType.INSECT);
assertColor(playerA, "Keranos, God of Storms", "{G}{B}", true);
assertColor(playerA, "Baneslayer Angel", "{R}{U}", false);
// Grist, the Hunger Tide is not a Creature (It would become a 1/1 Insect if animated somehow with an effect preserving its p/t/subtype)
assertType("Grist, the Hunger Tide", CardType.CREATURE, false);
assertType("Grist, the Hunger Tide", CardType.PLANESWALKER, true);
assertNotSubtype("Grist, the Hunger Tide", SubType.INSECT);
assertSubtype("Grist, the Hunger Tide", SubType.GRIST);
assertColor(playerA, "Grist, the Hunger Tide", "{G}{B}", true);
}
}