diff --git a/Mage.Sets/src/mage/cards/r/RaulTroubleShooter.java b/Mage.Sets/src/mage/cards/r/RaulTroubleShooter.java new file mode 100644 index 00000000000..030bc31eddb --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RaulTroubleShooter.java @@ -0,0 +1,116 @@ +package mage.cards.r; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.common.CastFromGraveyardOnceEachTurnAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.common.MillCardsEachPlayerEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.watchers.Watcher; + +import java.util.*; + +/** + * @author Susucr + */ +public final class RaulTroubleShooter extends CardImpl { + + private static final FilterCard filter = new FilterCard("a spell from among cards in your graveyard that were milled this turn"); + + static { + filter.add(RaulTroubleShooterPredicate.instance); + } + + public RaulTroubleShooter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ZOMBIE); + this.subtype.add(SubType.MUTANT); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // Once during each of your turns, you may cast a spell from among cards in your graveyard that were milled this turn. + this.addAbility(new CastFromGraveyardOnceEachTurnAbility(filter), new RaulTroubleShooterWatcher()); + + // {T}: Each player mills a card. + this.addAbility(new SimpleActivatedAbility( + new MillCardsEachPlayerEffect(1, TargetController.ANY), + new TapSourceCost() + )); + } + + private RaulTroubleShooter(final RaulTroubleShooter card) { + super(card); + } + + @Override + public RaulTroubleShooter copy() { + return new RaulTroubleShooter(this); + } +} + +enum RaulTroubleShooterPredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return RaulTroubleShooterWatcher.wasMilledThisTurn( + input.getPlayerId(), + new MageObjectReference(input.getObject().getMainCard(), game), + game + ); + } +} + +class RaulTroubleShooterWatcher extends Watcher { + + // player -> set of Cards mor (the main card's mor) milled this turn. + private final Map> milledThisTurn = new HashMap<>(); + + RaulTroubleShooterWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.MILLED_CARD) { + return; + } + Card card = game.getCard(event.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; + } + milledThisTurn.computeIfAbsent(event.getPlayerId(), k -> new HashSet<>()); + milledThisTurn.get(event.getPlayerId()).add(new MageObjectReference(mainCard, game)); + } + + @Override + public void reset() { + super.reset(); + milledThisTurn.clear(); + } + + static boolean wasMilledThisTurn(UUID playerId, MageObjectReference morMainCard, Game game) { + RaulTroubleShooterWatcher watcher = game.getState().getWatcher(RaulTroubleShooterWatcher.class); + return watcher != null && watcher + .milledThisTurn + .getOrDefault(playerId, Collections.emptySet()) + .contains(morMainCard); + } +} diff --git a/Mage.Sets/src/mage/sets/Fallout.java b/Mage.Sets/src/mage/sets/Fallout.java index 5cc0cd5d564..4ae3c5c69a0 100644 --- a/Mage.Sets/src/mage/sets/Fallout.java +++ b/Mage.Sets/src/mage/sets/Fallout.java @@ -249,6 +249,7 @@ public final class Fallout extends ExpansionSet { cards.add(new SetCardInfo("Radstorm", 37, Rarity.RARE, mage.cards.r.Radstorm.class)); cards.add(new SetCardInfo("Rampant Growth", 204, Rarity.COMMON, mage.cards.r.RampantGrowth.class)); cards.add(new SetCardInfo("Rancor", 205, Rarity.UNCOMMON, mage.cards.r.Rancor.class)); + cards.add(new SetCardInfo("Raul, Trouble Shooter", 115, Rarity.UNCOMMON, mage.cards.r.RaulTroubleShooter.class)); cards.add(new SetCardInfo("Ravages of War", 354, Rarity.MYTHIC, mage.cards.r.RavagesOfWar.class)); cards.add(new SetCardInfo("Razortide Bridge", 281, Rarity.COMMON, mage.cards.r.RazortideBridge.class)); cards.add(new SetCardInfo("Red Death, Shipwrecker", 116, Rarity.RARE, mage.cards.r.RedDeathShipwrecker.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/RaulTroubleShooterTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/RaulTroubleShooterTest.java new file mode 100644 index 00000000000..9a0532ec8d5 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/pip/RaulTroubleShooterTest.java @@ -0,0 +1,159 @@ +package org.mage.test.cards.single.pip; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class RaulTroubleShooterTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.r.RaulTroubleShooter Raul, Trouble Shooter} {1}{U}{B} + * Legendary Creature — Zombie Mutant Rogue + * Once during each of your turns, you may cast a spell from among cards in your graveyard that were milled this turn. + * {T}: Each player mills a card. (They each put the top card of their library into their graveyard.) + * 1/4 + */ + private static final String raul = "Raul, Trouble Shooter"; + + @Test + public void test_Cast_Simple() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, raul); + addCard(Zone.LIBRARY, playerA, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + activateAbility(1, PhaseStep.UPKEEP, playerA, "{T}: Each player"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Grizzly Bears"); // Can cast from graveyard. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Grizzly Bears", 1); + assertTappedCount("Forest", true, 2); + } + + @Test + public void test_Cast_Split() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, raul); + addCard(Zone.BATTLEFIELD, playerB, "Memnite"); + addCard(Zone.LIBRARY, playerA, "Fire // Ice"); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + activateAbility(1, PhaseStep.UPKEEP, playerA, "{T}: Each player"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ice", "Memnite"); // Can cast from graveyard. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertHandCount(playerA, 1); // drawn from Ice + assertGraveyardCount(playerA, "Fire // Ice", 1); + assertTappedCount("Island", true, 2); + assertTappedCount("Memnite", true, 1); + } + + + @Test + public void test_Cast_Simple_Instant_YourTurn() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, raul); + addCard(Zone.LIBRARY, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + activateAbility(1, PhaseStep.UPKEEP, playerA, "{T}: Each player"); + checkPlayableAbility("1: Can cast the turn milled", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", true); + checkPlayableAbility("2: Cannot cast opponent card", 1, PhaseStep.PRECOMBAT_MAIN, playerB, "Cast Lightning Bolt", false); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN, playerA); + checkPlayableAbility("3: Cannot cast twice", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertLife(playerB, 20 - 3); + assertTappedCount("Mountain", true, 1); + } + + @Test + public void test_CannotCast_Instant_NotYourTurn() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, raul); + addCard(Zone.LIBRARY, playerA, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + + activateAbility(2, PhaseStep.UPKEEP, playerA, "{T}: Each player"); + checkPlayableAbility("1: Cannot cast on opponent's turn", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + checkPlayableAbility("2: Cannot cast not your card", 2, PhaseStep.PRECOMBAT_MAIN, playerB, "Cast Lightning Bolt", false); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Lightning Bolt", 1); + } + + @Test + public void test_CannotCast_OtherTurn() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.BATTLEFIELD, playerA, raul); + addCard(Zone.LIBRARY, playerA, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + + activateAbility(1, PhaseStep.UPKEEP, playerA, "{T}: Each player"); + + checkPlayableAbility("Can cast the turn milled", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", true); + checkPlayableAbility("Cannot cast after that", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Grizzly Bears", false); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Grizzly Bears", 1); + } + + @Test + public void test_Cast_MilledBefore() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.HAND, playerA, raul); + addCard(Zone.LIBRARY, playerA, "Bump in the Night"); // {B} Target opponent loses 3 life. + addCard(Zone.LIBRARY, playerA, "Memnite"); + addCard(Zone.HAND, playerA, "Thought Scour"); // {U} Target player mills two cards. Draw a card. + addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 6); + + castSpell(1, PhaseStep.UPKEEP, playerA, "Thought Scour", playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, raul, true); + + checkPlayableAbility("1: Can cast milled Bump in the Night", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Bump in the Night", true); + checkPlayableAbility("2: Can cast milled Memnite", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Memnite", true); + checkPlayableAbility("3: Can not cast not milled Thought Scour", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Thought Scour", false); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bump in the Night", playerB); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + checkPlayableAbility("4: Can not cast milled Bump in the Night", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Bump in the Night", false); + checkPlayableAbility("5: Can not cast milled Memnite", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Memnite", false); + checkPlayableAbility("6: Can not cast not milled Thought Scour", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Thought Scour", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, 3); + assertLife(playerB, 20 - 3); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index ca4610c26a1..d1f601a648b 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -102,6 +102,10 @@ public class GameEvent implements Serializable { */ CLASH, CLASHED, DAMAGE_PLAYER, + /* MILL_CARDS + playerId the id of the player milling the card (not the source's controller) + targetId the id of the card milled + */ MILL_CARDS, MILLED_CARD, MILLED_CARDS,