diff --git a/Mage.Sets/src/mage/cards/s/StarvingRevenant.java b/Mage.Sets/src/mage/cards/s/StarvingRevenant.java new file mode 100644 index 00000000000..8d0a80c749d --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StarvingRevenant.java @@ -0,0 +1,93 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DrawCardControllerTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.DescendCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.AbilityWord; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class StarvingRevenant extends CardImpl { + + public StarvingRevenant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{B}"); + + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // When Starving Revenant enters the battlefield, surveil 2. Then for each card you put on top of your library, you draw a card and you lose 3 life. + this.addAbility(new EntersBattlefieldTriggeredAbility(new StarvingRevenantEffect())); + + // Descend 8 -- Whenever you draw a card, if there are eight or more permanent cards in your graveyard, target opponent loses 1 life and you gain 1 life. + Ability ability = new ConditionalInterveningIfTriggeredAbility( + new DrawCardControllerTriggeredAbility(new LoseLifeTargetEffect(1), false), + DescendCondition.EIGHT, "Whenever you draw a card, " + + "if there are eight or more permanent cards in your graveyard, " + + "target opponent loses 1 life and you gain 1 life." + ); + ability.addEffect(new GainLifeEffect(1)); + ability.addTarget(new TargetOpponent()); + this.addAbility(ability.addHint(DescendCondition.getHint()).setAbilityWord(AbilityWord.DESCEND_8)); + } + + private StarvingRevenant(final StarvingRevenant card) { + super(card); + } + + @Override + public StarvingRevenant copy() { + return new StarvingRevenant(this); + } +} + +class StarvingRevenantEffect extends OneShotEffect { + + StarvingRevenantEffect() { + super(Outcome.Benefit); + staticText = "surveil 2. Then for each card you put on top of your library, you draw a card and you lose 3 life"; + } + + private StarvingRevenantEffect(final StarvingRevenantEffect effect) { + super(effect); + } + + @Override + public StarvingRevenantEffect copy() { + return new StarvingRevenantEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + + int amountOnTop = controller.doSurveil(2, source, game).getNumberPutOnTop(); + for (int i = 0; i < amountOnTop; ++i) { + controller.drawCards(1, source, game); + controller.loseLife(3, game, source, false); + } + return true; + } + +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java index 9677d56a336..8834d1570a4 100644 --- a/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java +++ b/Mage.Sets/src/mage/sets/TheLostCavernsOfIxalan.java @@ -162,6 +162,7 @@ public final class TheLostCavernsOfIxalan extends ExpansionSet { cards.add(new SetCardInfo("Spyglass Siren", 78, Rarity.UNCOMMON, mage.cards.s.SpyglassSiren.class)); cards.add(new SetCardInfo("Squirming Emergence", 241, Rarity.RARE, mage.cards.s.SquirmingEmergence.class)); cards.add(new SetCardInfo("Stalactite Stalker", 122, Rarity.RARE, mage.cards.s.StalactiteStalker.class)); + cards.add(new SetCardInfo("Starving Revenant", 123, Rarity.RARE, mage.cards.s.StarvingRevenant.class)); cards.add(new SetCardInfo("Staunch Crewmate", 79, Rarity.UNCOMMON, mage.cards.s.StaunchCrewmate.class)); cards.add(new SetCardInfo("Stinging Cave Crawler", 124, Rarity.UNCOMMON, mage.cards.s.StingingCaveCrawler.class)); cards.add(new SetCardInfo("Sunbird Effigy", 262, Rarity.UNCOMMON, mage.cards.s.SunbirdEffigy.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/StarvingRevenantTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/StarvingRevenantTest.java new file mode 100644 index 00000000000..0de2b640260 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/StarvingRevenantTest.java @@ -0,0 +1,87 @@ +package org.mage.test.cards.single.lci; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.player.TestPlayer; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class StarvingRevenantTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.s.StarvingRevenant}
+ * Starving Revenant {2}{B}{B}
+ * Creature — Spirit Horror
+ * When Starving Revenant enters the battlefield, surveil 2. Then for each card you put on top of your library, you draw a card and you lose 3 life.
+ * Descend 8 — Whenever you draw a card, if there are eight or more permanent cards in your graveyard, target opponent loses 1 life and you gain 1 life.
+ * 4/4 + */ + private static final String revenant = "Starving Revenant"; + + @Test + public void surveil_both_graveyard() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.HAND, playerA, revenant); + addCard(Zone.LIBRARY, playerA, "Plains"); + addCard(Zone.LIBRARY, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, revenant); + addTarget(playerA, "Plains^Island"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20); + assertGraveyardCount(playerA, 2); + assertHandCount(playerA, 0); + } + + @Test + public void surveil_one_on_top() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.HAND, playerA, revenant); + addCard(Zone.LIBRARY, playerA, "Plains"); + addCard(Zone.LIBRARY, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, revenant); + addTarget(playerA, "Plains"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 3); + assertGraveyardCount(playerA, 1); + assertHandCount(playerA, 1); + } + + @Test + public void surveil_both_on_top() { + setStrictChooseMode(true); + skipInitShuffling(); + + addCard(Zone.HAND, playerA, revenant); + addCard(Zone.LIBRARY, playerA, "Plains"); + addCard(Zone.LIBRARY, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, revenant); + addTarget(playerA, TestPlayer.TARGET_SKIP); + setChoice(playerA, "Plains"); // Plains put on top first + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20 - 3 * 2); + assertGraveyardCount(playerA, 0); + assertHandCount(playerA, 2); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index 7485abfb40f..2f1f11b54e7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -1145,7 +1145,7 @@ public class TestPlayer implements Player { Assert.fail(action.getActionName() + " - can't find permanent to check: " + cardName); return null; } - + private void printStart(Game game, String name) { System.out.println("\n" + game.toString()); System.out.println(name + ":"); @@ -2867,7 +2867,7 @@ public class TestPlayer implements Player { @Override public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, - int min, int max, MultiAmountType type, Game game) { + int min, int max, MultiAmountType type, Game game) { assertAliasSupportInChoices(false); int needCount = messages.size(); @@ -3171,12 +3171,12 @@ public class TestPlayer implements Player { } @Override - public Map>> getCastSourceIdManaCosts() { + public Map>> getCastSourceIdManaCosts() { return computerPlayer.getCastSourceIdManaCosts(); } @Override - public Map>> getCastSourceIdCosts() { + public Map>> getCastSourceIdCosts() { return computerPlayer.getCastSourceIdCosts(); } @@ -4372,10 +4372,8 @@ public class TestPlayer implements Player { } @Override - public boolean surveil(int value, Ability source, - Game game - ) { - return computerPlayer.surveil(value, source, game); + public SurveilResult doSurveil(int value, Ability source, Game game) { + return computerPlayer.doSurveil(value, source, game); } @Override diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index dca7330c7da..fe0d5232ae0 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -986,7 +986,7 @@ public class PlayerStub implements Player { @Override public List getMultiAmountWithIndividualConstraints(Outcome outcome, List messages, - int min, int max, MultiAmountType type, Game game) { + int min, int max, MultiAmountType type, Game game) { return null; } @@ -1281,7 +1281,7 @@ public class PlayerStub implements Player { } @Override - public Map>> getCastSourceIdCosts() { + public Map>> getCastSourceIdCosts() { return null; } @@ -1346,8 +1346,8 @@ public class PlayerStub implements Player { } @Override - public boolean surveil(int value, Ability source, Game game) { - return false; + public SurveilResult doSurveil(int value, Ability source, Game game) { + return SurveilResult.noSurveil(); } @Override diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 2965b1da565..363a44ab517 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -1108,7 +1108,48 @@ public interface Player extends MageItem, Copyable { boolean scry(int value, Ability source, Game game); - boolean surveil(int value, Ability source, Game game); + /** + * result of the doSurveil action. + * Sometimes more info is needed for the caller after the surveil is done. + */ + class SurveilResult { + private final boolean surveilled; + private final int numberInGraveyard; // how many cards were put into the graveyard + private final int numberOnTop; // how many cards were put into the graveyard + + private SurveilResult(boolean surveilled, int inGrave, int onTop) { + this.surveilled = surveilled; + this.numberInGraveyard = inGrave; + this.numberOnTop = onTop; + } + + public static SurveilResult noSurveil() { + return new SurveilResult(false, 0, 0); + } + + public static SurveilResult surveil(int inGrave, int onTop) { + return new SurveilResult(true, inGrave, onTop); + } + + public boolean hasSurveilled() { + return this.surveilled; + } + + public int getNumberPutInGraveyard() { + return this.numberInGraveyard; + } + + public int getNumberPutOnTop() { + return this.numberOnTop; + } + } + + SurveilResult doSurveil(int value, Ability source, Game game); + + default boolean surveil(int value, Ability source, Game game) { + SurveilResult result = doSurveil(value, source, game); + return result.hasSurveilled(); + } /** * Only used for test player for pre-setting targets diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index c110093d8b8..9854d1653ec 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -287,13 +287,13 @@ public abstract class PlayerImpl implements Player, Serializable { } for (Entry>> entry : player.getCastSourceIdManaCosts().entrySet()) { this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>()); - for(Entry> subEntry : entry.getValue().entrySet()) { + for (Entry> subEntry : entry.getValue().entrySet()) { this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy()); } } for (Entry>> entry : player.getCastSourceIdCosts().entrySet()) { this.castSourceIdCosts.put(entry.getKey(), new HashMap<>()); - for(Entry> subEntry : entry.getValue().entrySet()) { + for (Entry> subEntry : entry.getValue().entrySet()) { this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy()); } } @@ -381,13 +381,13 @@ public abstract class PlayerImpl implements Player, Serializable { } for (Entry>> entry : player.getCastSourceIdManaCosts().entrySet()) { this.castSourceIdManaCosts.put(entry.getKey(), new HashMap<>()); - for(Entry> subEntry : entry.getValue().entrySet()) { + for (Entry> subEntry : entry.getValue().entrySet()) { this.castSourceIdManaCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy()); } } for (Entry>> entry : player.getCastSourceIdCosts().entrySet()) { this.castSourceIdCosts.put(entry.getKey(), new HashMap<>()); - for(Entry> subEntry : entry.getValue().entrySet()) { + for (Entry> subEntry : entry.getValue().entrySet()) { this.castSourceIdCosts.get(entry.getKey()).put(subEntry.getKey(), subEntry.getValue() == null ? null : subEntry.getValue().copy()); } } @@ -3551,7 +3551,7 @@ public abstract class PlayerImpl implements Player, Serializable { } // ALTERNATIVE COST FROM dynamic effects - for(MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) { + for (MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) { ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier); Costs costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier); @@ -5134,14 +5134,15 @@ public abstract class PlayerImpl implements Player, Serializable { } @Override - public boolean surveil(int value, Ability source, Game game) { + public SurveilResult doSurveil(int value, Ability source, Game game) { GameEvent event = new GameEvent(GameEvent.EventType.SURVEIL, getId(), source, getId(), value, true); if (game.replaceEvent(event)) { - return false; + return SurveilResult.noSurveil(); } game.informPlayers(getLogName() + " surveils " + event.getAmount() + CardUtil.getSourceLogName(game, source)); Cards cards = new CardsImpl(); cards.addAllCards(getLibrary().getTopCards(game, event.getAmount())); + int totalCount = cards.size(); if (!cards.isEmpty()) { TargetCard target = new TargetCard(0, cards.size(), Zone.LIBRARY, new FilterCard("card" + (cards.size() == 1 ? "" : "s") @@ -5152,7 +5153,7 @@ public abstract class PlayerImpl implements Player, Serializable { putCardsOnTopOfLibrary(cards, game, source, true); } game.fireEvent(new GameEvent(GameEvent.EventType.SURVEILED, getId(), source, getId(), event.getAmount(), true)); - return true; + return SurveilResult.surveil(totalCount - cards.size(), cards.size()); } @Override