diff --git a/Mage.Sets/src/mage/cards/c/CoramTheUndertaker.java b/Mage.Sets/src/mage/cards/c/CoramTheUndertaker.java new file mode 100644 index 00000000000..36148d5d5af --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CoramTheUndertaker.java @@ -0,0 +1,272 @@ +package mage.cards.c; + +import mage.MageIdentifier; +import mage.MageInt; +import mage.MageObject; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.MillCardsEachPlayerEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import mage.watchers.Watcher; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * @author Susucr + */ +public final class CoramTheUndertaker extends CardImpl { + + public CoramTheUndertaker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{R}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(0); + this.toughness = new MageInt(5); + + // Coram, the Undertaker gets +X/+0, where X is the greatest power among creature cards in all graveyards. + this.addAbility(new SimpleStaticAbility(new BoostSourceEffect( + CoramTheUndertakerValue.instance, StaticValue.get(0), Duration.WhileOnBattlefield + ))); + + // Whenever Coram attacks, each player mills a card. + this.addAbility(new AttacksTriggeredAbility( + new MillCardsEachPlayerEffect(1, TargetController.EACH_PLAYER), false + )); + + // During each of your turns, you may play a land and cast a spell from among cards in graveyards that were put there from libraries this turn. + this.addAbility(new CoramTheUndertakerStaticAbility()); + } + + private CoramTheUndertaker(final CoramTheUndertaker card) { + super(card); + } + + @Override + public CoramTheUndertaker copy() { + return new CoramTheUndertaker(this); + } +} + +enum CoramTheUndertakerValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return game.getState() + .getPlayersInRange(sourceAbility.getControllerId(), game) + .stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .map(Player::getGraveyard) + .flatMap(graveyard -> graveyard.getCards(game).stream()) + .filter(Objects::nonNull) + .filter(card -> card.isCreature(game)) + .map(MageObject::getPower) + .mapToInt(MageInt::getValue) + .max() + .orElse(0); + } + + @Override + public CoramTheUndertakerValue copy() { + return instance; + } + + @Override + public String getMessage() { + return "the greatest power among creature cards in all graveyards"; + } + + @Override + public String toString() { + return "X"; + } +} + +class CoramTheUndertakerStaticAbility extends SimpleStaticAbility { + + CoramTheUndertakerStaticAbility() { + super(new CoramTheUndertakerPlayLandFromGraveyardEffect()); + addEffect(new CoramTheUndertakerCastSpellFromGraveyardEffect()); + setIdentifier(MageIdentifier.CoramTheUndertakerWatcher); + addWatcher(new CoramTheUndertakerWatcher()); + } + + private CoramTheUndertakerStaticAbility(final CoramTheUndertakerStaticAbility ability) { + super(ability); + } + + @Override + public CoramTheUndertakerStaticAbility copy() { + return new CoramTheUndertakerStaticAbility(this); + } + + @Override + public String getRule() { + return "During each of your turns, you may play a land and cast a spell from among cards in graveyards that were put there from libraries this turn"; + } +} + +class CoramTheUndertakerPlayLandFromGraveyardEffect extends AsThoughEffectImpl { + + CoramTheUndertakerPlayLandFromGraveyardEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "During each of your turns, you may play a land from among cards in graveyards that were put there from libraries this turn."; + } + + private CoramTheUndertakerPlayLandFromGraveyardEffect(final CoramTheUndertakerPlayLandFromGraveyardEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public CoramTheUndertakerPlayLandFromGraveyardEffect copy() { + return new CoramTheUndertakerPlayLandFromGraveyardEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + if (!source.isControlledBy(affectedControllerId) + || !affectedControllerId.equals(game.getActivePlayerId()) // only during your turns (e.g. prevent flash creatures) + || !Zone.GRAVEYARD.equals(game.getState().getZone(objectId))) { + return false; + } + CoramTheUndertakerWatcher watcher = game.getState().getWatcher(CoramTheUndertakerWatcher.class); + Card card = game.getCard(objectId); + return card != null + && watcher != null + && watcher.cardPutFromGraveyardThisTurn(new MageObjectReference(card.getMainCard(), game)) + && card.isLand(game) + && !watcher.landPlayedFromGraveyard(source, game); + } +} + +class CoramTheUndertakerCastSpellFromGraveyardEffect extends AsThoughEffectImpl { + + CoramTheUndertakerCastSpellFromGraveyardEffect() { + super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "During each of your turns, you may cast a spell from among cards in graveyards that were put there from libraries this turn."; + } + + private CoramTheUndertakerCastSpellFromGraveyardEffect(final CoramTheUndertakerCastSpellFromGraveyardEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public CoramTheUndertakerCastSpellFromGraveyardEffect copy() { + return new CoramTheUndertakerCastSpellFromGraveyardEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + if (!source.isControlledBy(affectedControllerId) + || !affectedControllerId.equals(game.getActivePlayerId()) // only during your turns (e.g. prevent flash creatures) + || !Zone.GRAVEYARD.equals(game.getState().getZone(objectId))) { + return false; + } + CoramTheUndertakerWatcher watcher = game.getState().getWatcher(CoramTheUndertakerWatcher.class); + Card card = game.getCard(objectId); + return card != null + && watcher != null + && watcher.cardPutFromGraveyardThisTurn(new MageObjectReference(card.getMainCard(), game)) + && card.isPermanent(game) + && !watcher.spellCastFromGraveyard(source, game); + } +} + +/** + * Holds track of both consumed effects, as well as cards valid to be cast by Coram. + */ +class CoramTheUndertakerWatcher extends Watcher { + + // mor -> has this mor's effect been used to play a land this turn + private final Set landPlayedForSource = new HashSet<>(); + // mor -> has this mor's effect been used to cast a permanent spell this turn + private final Set spellCastForSource = new HashSet<>(); + + // mor of cards in graveyard that were put there from library this turn. + private final Set cardsAllowedToBePlayedOrCast = new HashSet<>(); + + public CoramTheUndertakerWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.ZONE_CHANGE) { + ZoneChangeEvent zce = (ZoneChangeEvent) event; + if (zce == null || !Zone.LIBRARY.equals(zce.getFromZone()) || !Zone.GRAVEYARD.equals(zce.getToZone())) { + return; + } + Card card = game.getCard(zce.getTargetId()); + if (card == null) { + return; + } + Card mainCard = card.getMainCard(); + if (game.getState().getZone(mainCard.getId()) != Zone.GRAVEYARD) { + // Ensure that the current zone is indeed the graveyard + return; + } + cardsAllowedToBePlayedOrCast.add(new MageObjectReference(mainCard, game)); + return; + } + if (event.getAdditionalReference() == null + || !MageIdentifier.CoramTheUndertakerWatcher.equals(event.getAdditionalReference().getApprovingAbility().getIdentifier())) { + return; + } + if (event.getType() == GameEvent.EventType.LAND_PLAYED) { + landPlayedForSource.add(event.getAdditionalReference().getApprovingMageObjectReference()); + } + if (event.getType() == GameEvent.EventType.SPELL_CAST) { + spellCastForSource.add(event.getAdditionalReference().getApprovingMageObjectReference()); + } + } + + @Override + public void reset() { + landPlayedForSource.clear(); + spellCastForSource.clear(); + cardsAllowedToBePlayedOrCast.clear(); + super.reset(); + } + + public boolean cardPutFromGraveyardThisTurn(MageObjectReference mor) { + return cardsAllowedToBePlayedOrCast.contains(mor); + } + + public boolean landPlayedFromGraveyard(Ability source, Game game) { + return landPlayedForSource.contains(new MageObjectReference(source.getSourceObject(game), game)); + } + + public boolean spellCastFromGraveyard(Ability source, Game game) { + return spellCastForSource.contains(new MageObjectReference(source.getSourceObject(game), game)); + } +} diff --git a/Mage.Sets/src/mage/sets/ModernHorizons3Commander.java b/Mage.Sets/src/mage/sets/ModernHorizons3Commander.java index 22b40700460..6d1a625c5ad 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons3Commander.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons3Commander.java @@ -81,6 +81,7 @@ public final class ModernHorizons3Commander extends ExpansionSet { cards.add(new SetCardInfo("Confiscation Coup", 178, Rarity.RARE, mage.cards.c.ConfiscationCoup.class)); cards.add(new SetCardInfo("Conversion Apparatus", 76, Rarity.RARE, mage.cards.c.ConversionApparatus.class)); cards.add(new SetCardInfo("Copy Land", 47, Rarity.RARE, mage.cards.c.CopyLand.class)); + cards.add(new SetCardInfo("Coram, the Undertaker", 7, Rarity.MYTHIC, mage.cards.c.CoramTheUndertaker.class)); cards.add(new SetCardInfo("Corrupted Crossroads", 332, Rarity.RARE, mage.cards.c.CorruptedCrossroads.class)); cards.add(new SetCardInfo("Coveted Jewel", 287, Rarity.RARE, mage.cards.c.CovetedJewel.class)); cards.add(new SetCardInfo("Crib Swap", 168, Rarity.UNCOMMON, mage.cards.c.CribSwap.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/CoramTheUndertakerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/CoramTheUndertakerTest.java new file mode 100644 index 00000000000..967cae99884 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/CoramTheUndertakerTest.java @@ -0,0 +1,105 @@ +package org.mage.test.cards.single.m3c; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class CoramTheUndertakerTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.c.CoramTheUndertaker Coram, the Undertaker} {1}{B}{R}{G} + * Legendary Creature — Human Warrior + * Coram, the Undertaker gets +X/+0, where X is the greatest power among creature cards in all graveyards. + * Whenever Coram attacks, each player mills a card. + * During each of your turns, you may play a land and cast a spell from among cards in graveyards that were put there from libraries this turn. + * 0/5 + */ + private static final String coram = "Coram, the Undertaker"; + + @Test + public void test_DoubleMode() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.GRAVEYARD, playerA, "Taiga"); + addCard(Zone.GRAVEYARD, playerA, "Ornithopter"); + addCard(Zone.LIBRARY, playerB, "Plains"); + addCard(Zone.LIBRARY, playerA, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, coram); + + checkPT("1: Coram is 0/5", 1, PhaseStep.PRECOMBAT_MAIN, playerA, coram, 0, 5); + + attack(1, playerA, coram, playerB); + + checkPT("2: Coram is 2/5", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, coram, 2, 5); + + checkPlayableAbility("3: can not play Taiga, was not milled", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Play Taiga", false); + checkPlayableAbility("3: can play Plains, was milled", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Play Plains", true); + checkPlayableAbility("3: can not cast Ornithopter, was not milled", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Ornithopter", false); + checkPlayableAbility("3: can cast Grizzly Bears, was milled", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Grizzly Bears", true); + playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains"); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Grizzly Bears"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, coram, 0, 5); + assertPermanentCount(playerA, "Grizzly Bears", 1); + assertPermanentCount(playerA, "Plains", 1); + assertGraveyardCount(playerA, "Ornithopter", 1); + assertGraveyardCount(playerA, "Taiga", 1); + } + + @Test + public void test_CantDoublePlayLand() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.LIBRARY, playerB, "Plains"); + addCard(Zone.LIBRARY, playerA, "Taiga"); + addCard(Zone.BATTLEFIELD, playerA, coram); + addCard(Zone.BATTLEFIELD, playerA, "Exploration"); + + attack(1, playerA, coram, playerB); + + checkPlayableAbility("1: can play Taiga, was milled", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Play Taiga", true); + checkPlayableAbility("1: can play Plains, was milled", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Play Plains", true); + playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Plains"); + checkPlayableAbility("2: can not play Taiga, limit 1/turn", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Play Taiga", false); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Plains", 1); + assertGraveyardCount(playerA, "Taiga", 1); + } + + @Test + public void test_CantDoubleCastSpell() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.LIBRARY, playerB, "Bear Cub"); + addCard(Zone.LIBRARY, playerA, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerA, coram); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + + attack(1, playerA, coram, playerB); + + checkPlayableAbility("1: can cast Bear Cub, was milled", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Bear Cub", true); + checkPlayableAbility("1: can cast Grizzly Bears, was milled", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Grizzly Bears", true); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Bear Cub"); + checkPlayableAbility("2: can not cast Grizzly Bears, limit 1/turn", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Grizzly Bears", false); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Bear Cub", 1); + assertGraveyardCount(playerA, "Grizzly Bears", 1); + } +} diff --git a/Mage/src/main/java/mage/MageIdentifier.java b/Mage/src/main/java/mage/MageIdentifier.java index a160f2fdf4a..fe8a52deb96 100644 --- a/Mage/src/main/java/mage/MageIdentifier.java +++ b/Mage/src/main/java/mage/MageIdentifier.java @@ -34,6 +34,7 @@ public enum MageIdentifier { KaghaShadowArchdruidWatcher, CourtOfLocthwainWatcher("Without paying manacost"), LaraCroftTombRaiderWatcher, + CoramTheUndertakerWatcher, // ----------------------------// // alternate casts //