From 2a5dd4103c111b102a57c87bbda98cf842754b09 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Thu, 31 Aug 2023 01:15:56 +0200 Subject: [PATCH] [WOE] Implement Ashiok, Wicked Manipulator (#10909) * [WOE] Implement Ashiok, Wicket Manipulator * Add Ashiok's abilities * basic pay life replacement tests * many tests later * add warning on token expecting watcher * apply review * rework text generation --- .../mage/cards/a/AshiokWickedManipulator.java | 119 +++++++ Mage.Sets/src/mage/cards/o/OblivionSower.java | 2 +- .../src/mage/cards/r/RavenGuildMaster.java | 2 +- .../src/mage/cards/s/SireOfStagnation.java | 2 +- .../src/mage/cards/t/ThoughtHarvester.java | 2 +- Mage.Sets/src/mage/sets/WildsOfEldraine.java | 1 + .../woe/AshiokWickedManipulatorTest.java | 308 ++++++++++++++++++ .../WasCardExiledThisTurnCondition.java | 30 ++ .../TotalCardsExiledOwnedManaValue.java | 52 +++ ...xileCardsFromTopOfLibraryTargetEffect.java | 36 +- Mage/src/main/java/mage/game/Exile.java | 2 +- .../main/java/mage/game/events/GameEvent.java | 2 +- ...AshiokWickedManipulatorNightmareToken.java | 52 +++ Mage/src/main/java/mage/util/CardUtil.java | 5 + .../common/CardsExiledThisTurnWatcher.java | 38 +++ 15 files changed, 634 insertions(+), 19 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/a/AshiokWickedManipulator.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/woe/AshiokWickedManipulatorTest.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/WasCardExiledThisTurnCondition.java create mode 100644 Mage/src/main/java/mage/abilities/dynamicvalue/common/TotalCardsExiledOwnedManaValue.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/AshiokWickedManipulatorNightmareToken.java create mode 100644 Mage/src/main/java/mage/watchers/common/CardsExiledThisTurnWatcher.java diff --git a/Mage.Sets/src/mage/cards/a/AshiokWickedManipulator.java b/Mage.Sets/src/mage/cards/a/AshiokWickedManipulator.java new file mode 100644 index 00000000000..963a11f24f4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AshiokWickedManipulator.java @@ -0,0 +1,119 @@ +package mage.cards.a; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.TotalCardsExiledOwnedManaValue; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.ExileCardsFromTopOfLibraryTargetEffect; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +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.permanent.token.AshiokWickedManipulatorNightmareToken; +import mage.players.Player; +import mage.target.TargetPlayer; +import mage.watchers.common.CardsExiledThisTurnWatcher; + +import java.util.Set; +import java.util.UUID; + +/** + * @author Susucr + */ +public final class AshiokWickedManipulator extends CardImpl { + + public AshiokWickedManipulator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{3}{B}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ASHIOK); + this.setStartingLoyalty(5); + + // If you would pay life while your library has at least that many cards in it, exile that many cards from the top of your library instead. + this.addAbility(new SimpleStaticAbility(new AshiokWickedManipulatorReplacementEffect())); + + // +1: Look at the top two cards of your library. Exile one of them and put the other into your hand. + this.addAbility(new LoyaltyAbility( + new LookLibraryAndPickControllerEffect(2, 1, PutCards.EXILED, PutCards.HAND), + 1 + )); + + // -2: Create two 1/1 black Nightmare creature tokens with "At the beginning of combat on your turn, if a card was put into exile this turn, put a +1/+1 counter on this creature." + this.addAbility(new LoyaltyAbility( + new CreateTokenEffect(new AshiokWickedManipulatorNightmareToken(), 2), + -2 + ), new CardsExiledThisTurnWatcher()); + + // -7: Target player exiles the top X cards of their library, where X is the total mana value of cards you own in exile. + Ability ability = new LoyaltyAbility( + new ExileCardsFromTopOfLibraryTargetEffect(TotalCardsExiledOwnedManaValue.instance) + .setText("target player exiles the top X cards of their library, " + + "where X is the total mana value of cards you own in exile"), + -7 + ); + ability.addTarget(new TargetPlayer()); + ability.addHint(TotalCardsExiledOwnedManaValue.getHint()); + this.addAbility(ability); + } + + private AshiokWickedManipulator(final AshiokWickedManipulator card) { + super(card); + } + + @Override + public AshiokWickedManipulator copy() { + return new AshiokWickedManipulator(this); + } +} + +class AshiokWickedManipulatorReplacementEffect extends ReplacementEffectImpl { + + AshiokWickedManipulatorReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "If you would pay life while your library has at least that many cards in it, " + + "exile that many cards from the top of your library instead."; + } + + private AshiokWickedManipulatorReplacementEffect(final AshiokWickedManipulatorReplacementEffect effect) { + super(effect); + } + + @Override + public AshiokWickedManipulatorReplacementEffect copy() { + return new AshiokWickedManipulatorReplacementEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + + Set cards = player.getLibrary().getTopCards(game, event.getAmount()); + player.moveCardsToExile(cards, source, game, false, null, ""); + + return true; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.PAY_LIFE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + UUID playerId = source.getControllerId(); + if (!event.getPlayerId().equals(playerId)) { + return false; + } + + Player player = game.getPlayer(playerId); + return player != null && player.getLibrary().size() >= event.getAmount(); + } +} diff --git a/Mage.Sets/src/mage/cards/o/OblivionSower.java b/Mage.Sets/src/mage/cards/o/OblivionSower.java index cbab579cfba..9b25fdfea27 100644 --- a/Mage.Sets/src/mage/cards/o/OblivionSower.java +++ b/Mage.Sets/src/mage/cards/o/OblivionSower.java @@ -38,7 +38,7 @@ public final class OblivionSower extends CardImpl { this.toughness = new MageInt(8); // When you cast Oblivion Sower, target opponent exiles the top four cards of their library, then you may put any number of land cards that player owns from exile onto the battlefield under your control. - Ability ability = new CastSourceTriggeredAbility(new ExileCardsFromTopOfLibraryTargetEffect(4, "target opponent"), false); + Ability ability = new CastSourceTriggeredAbility(new ExileCardsFromTopOfLibraryTargetEffect(4), false); ability.addEffect(new OblivionSowerEffect()); ability.addTarget(new TargetOpponent()); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/r/RavenGuildMaster.java b/Mage.Sets/src/mage/cards/r/RavenGuildMaster.java index 7789d5eb018..bc30ea28a14 100644 --- a/Mage.Sets/src/mage/cards/r/RavenGuildMaster.java +++ b/Mage.Sets/src/mage/cards/r/RavenGuildMaster.java @@ -27,7 +27,7 @@ public final class RavenGuildMaster extends CardImpl { this.toughness = new MageInt(1); // Whenever Raven Guild Master deals combat damage to a player, that player exiles the top ten cards of their library. - this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(new ExileCardsFromTopOfLibraryTargetEffect(10, "that player"), false, true)); + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(new ExileCardsFromTopOfLibraryTargetEffect(10), false, true)); // Morph {2}{U}{U} this.addAbility(new MorphAbility(new ManaCostsImpl<>("{2}{U}{U}"))); diff --git a/Mage.Sets/src/mage/cards/s/SireOfStagnation.java b/Mage.Sets/src/mage/cards/s/SireOfStagnation.java index ec554483911..434096236a6 100644 --- a/Mage.Sets/src/mage/cards/s/SireOfStagnation.java +++ b/Mage.Sets/src/mage/cards/s/SireOfStagnation.java @@ -41,7 +41,7 @@ public final class SireOfStagnation extends CardImpl { // Whenever a land enters the battlefield under an opponent's control, that player exiles the top two cards of their library and you draw two cards. Ability ability = new EntersBattlefieldAllTriggeredAbility(Zone.BATTLEFIELD, - new ExileCardsFromTopOfLibraryTargetEffect(2, "that player"), filter, false, SetTargetPointer.PLAYER, rule, false); + new ExileCardsFromTopOfLibraryTargetEffect(2), filter, false, SetTargetPointer.PLAYER, rule, false); ability.addEffect(new DrawCardSourceControllerEffect(2)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/t/ThoughtHarvester.java b/Mage.Sets/src/mage/cards/t/ThoughtHarvester.java index 21decfc3b96..58c13ed9e98 100644 --- a/Mage.Sets/src/mage/cards/t/ThoughtHarvester.java +++ b/Mage.Sets/src/mage/cards/t/ThoughtHarvester.java @@ -42,7 +42,7 @@ public final class ThoughtHarvester extends CardImpl { this.addAbility(FlyingAbility.getInstance()); // Whenever you cast a colorless spell, target opponent exiles the top card of their library. - Ability ability = new SpellCastControllerTriggeredAbility(new ExileCardsFromTopOfLibraryTargetEffect(1, "target opponent"), filter, false); + Ability ability = new SpellCastControllerTriggeredAbility(new ExileCardsFromTopOfLibraryTargetEffect(1), filter, false); ability.addTarget(new TargetOpponent()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/sets/WildsOfEldraine.java b/Mage.Sets/src/mage/sets/WildsOfEldraine.java index be658c2fbc3..201b3ec1a8a 100644 --- a/Mage.Sets/src/mage/sets/WildsOfEldraine.java +++ b/Mage.Sets/src/mage/sets/WildsOfEldraine.java @@ -31,6 +31,7 @@ public final class WildsOfEldraine extends ExpansionSet { cards.add(new SetCardInfo("Armory Mice", 3, Rarity.COMMON, mage.cards.a.ArmoryMice.class)); cards.add(new SetCardInfo("Ash, Party Crasher", 201, Rarity.UNCOMMON, mage.cards.a.AshPartyCrasher.class)); cards.add(new SetCardInfo("Ashiok's Reaper", 79, Rarity.UNCOMMON, mage.cards.a.AshioksReaper.class)); + cards.add(new SetCardInfo("Ashiok, Wicked Manipulator", 78, Rarity.MYTHIC, mage.cards.a.AshiokWickedManipulator.class)); cards.add(new SetCardInfo("Asinine Antics", 42, Rarity.MYTHIC, mage.cards.a.AsinineAntics.class)); cards.add(new SetCardInfo("Back for Seconds", 80, Rarity.UNCOMMON, mage.cards.b.BackForSeconds.class)); cards.add(new SetCardInfo("Barrow Naughty", 81, Rarity.COMMON, mage.cards.b.BarrowNaughty.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/AshiokWickedManipulatorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/AshiokWickedManipulatorTest.java new file mode 100644 index 00000000000..d20993bf27b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/woe/AshiokWickedManipulatorTest.java @@ -0,0 +1,308 @@ +package org.mage.test.cards.single.woe; + +import mage.cards.Card; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.players.Player; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import java.util.Set; + +/** + * @author Susucr + */ +public class AshiokWickedManipulatorTest extends CardTestPlayerBase { + + /** + * Ashiok, Wicked Manipulator + * {3}{B}{B} + * Legendary Planeswalker — Ashiok + *

+ * If you would pay life while your library has at least that many cards in it, exile that many cards from the top of your library instead. + * +1: Look at the top two cards of your library. Exile one of them and put the other into your hand. + * −2: Create two 1/1 black Nightmare creature tokens with "At the beginning of combat on your turn, if a card was put into exile this turn, put a +1/+1 counter on this creature." + * −7: Target player exiles the top X cards of their library, where X is the total mana value of cards you own in exile. + *

+ * Loyalty: 5 + */ + private static final String ashiok = "Ashiok, Wicked Manipulator"; + + /** + * Final Payment + * {W}{B} + * Instant + *

+ * As an additional cost to cast this spell, pay 5 life or sacrifice a creature or enchantment. + *

+ * Destroy target creature. + */ + private static final String finalPayment = "Final Payment"; + + /** + * Well sometimes you do need a 2/2 vanilla with mana value 2. + */ + private static final String lion = "Silvercoat Lion"; + + /** + * Bolas's Citadel + * {3}{B}{B}{B} + * Legendary Artifact + *

+ * You may look at the top card of your library any time. + *

+ * You may play lands and cast spells from the top of your library. If you cast a spell this way, pay life equal to its mana value rather than pay its mana cost. + *

+ * {T}, Sacrifice ten nonland permanents: Each opponent loses 10 life. + */ + private static final String citadel = "Bolas's Citadel"; + + /** + * Lurking Evil + * {B}{B}{B} + * Enchantment + *

+ * Pay half your life, rounded up: Lurking Evil becomes a 4/4 Phyrexian Horror creature with flying. + */ + private static final String lurking = "Lurking Evil"; + + /** + * Arrogant Poet + * {1}{B} + * Creature — Human Warlock + *

+ * Whenever Arrogant Poet attacks, you may pay 2 life. If you do, it gains flying until end of turn. + */ + private static final String poet = "Arrogant Poet"; + + @Test + public void emptyALibrary() { + setStrictChooseMode(true); + + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, lion, 10); + + setStopAt(1, PhaseStep.UPKEEP); + execute(); + + assertExileCount(playerA, 0); + assertLibraryCount(playerA, 10); + assertLibraryCount(playerA, lion, 10); + } + + private void finalPaymentTest(int lionInLibrary, boolean replaced) { + setStrictChooseMode(true); + + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, lion, lionInLibrary); + + addCard(Zone.BATTLEFIELD, playerA, ashiok); + addCard(Zone.BATTLEFIELD, playerB, lion); + addCard(Zone.HAND, playerA, finalPayment); + addCard(Zone.BATTLEFIELD, playerA, "Scrubland", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, finalPayment, lion); + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerA, 20 - (replaced ? 0 : 5)); + assertPermanentCount(playerB, lion, 0); + assertExileCount(playerA, replaced ? 5 : 0); + assertLibraryCount(playerA, lionInLibrary - (replaced ? 5 : 0)); + assertGraveyardCount(playerB, lion, 1); // Lion + assertGraveyardCount(playerA, finalPayment, 1); + } + + @Test + public void finalPayment_0() { + finalPaymentTest(0, false); + } + + @Test + public void finalPayment_4() { + finalPaymentTest(4, false); + } + + @Test + public void finalPayment_5() { + finalPaymentTest(5, true); + } + + @Test + public void finalPayment_10() { + finalPaymentTest(10, true); + } + + @Test + public void ReplacementCitadel() { + setStrictChooseMode(true); + + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, lion, 10); + + addCard(Zone.BATTLEFIELD, playerA, ashiok); + addCard(Zone.BATTLEFIELD, playerA, citadel); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lion); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20); + assertPermanentCount(playerA, lion, 1); + assertExileCount(playerA, 2); + assertLibraryCount(playerA, 10 - 2 - 1); // Lion was cast from there, and 2 cards were exiled as payment. + } + + @Test + public void ReplacementLurkingEvil() { + setStrictChooseMode(true); + + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + int libraryCount = 18; + int exileCount = 0; + int lifeCount = 20; + addCard(Zone.LIBRARY, playerA, lion, libraryCount); + + addCard(Zone.BATTLEFIELD, playerA, ashiok); + addCard(Zone.BATTLEFIELD, playerA, lurking); + + activateAbility(1, PhaseStep.UPKEEP, playerA, "Pay half your life, rounded up:"); + libraryCount -= 10; + exileCount += 10; + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertLife(playerA, lifeCount); + assertExileCount(playerA, exileCount); + assertLibraryCount(playerA, libraryCount); + + activateAbility(1, PhaseStep.BEGIN_COMBAT, playerA, "Pay half your life, rounded up:"); + lifeCount -= 10; + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, lifeCount); + assertExileCount(playerA, exileCount); + assertLibraryCount(playerA, libraryCount); + + activateAbility(1, PhaseStep.END_TURN, playerA, "Pay half your life, rounded up:"); + libraryCount -= 5; + exileCount += 5; + + setStopAt(2, PhaseStep.UPKEEP); + execute(); + + assertLife(playerA, lifeCount); + assertExileCount(playerA, exileCount); + assertLibraryCount(playerA, libraryCount); + } + + @Test + public void ReplacementPoet() { + setStrictChooseMode(true); + + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, lion, 10); + + addCard(Zone.BATTLEFIELD, playerA, ashiok); + addCard(Zone.BATTLEFIELD, playerA, poet); + + attack(1, playerA, poet); + setChoice(playerA, true);// Yes to pay 2 life, those are replaced by cards exiled. + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 20); + assertExileCount(playerA, 2); + } + + @Test + public void TokensTrigger() { + setStrictChooseMode(true); + + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + for (int i = 0; i < 10; ++i) { + // Alternating, so choices are possible on Ashiok's +1 + addCard(Zone.LIBRARY, playerA, lion); + addCard(Zone.LIBRARY, playerA, poet); + } + + addCard(Zone.BATTLEFIELD, playerA, ashiok); + addCard(Zone.BATTLEFIELD, playerA, poet); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-2:"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, "Nightmare Token", 1, 1); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + addTarget(playerA, poet); + // 2 tokens, so stacking trigger. + setChoice(playerA, "At the beginning of combat on your turn, if a card was put " + + "into exile this turn, put a +1/+1 counter on this creature."); + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, "Nightmare Token", 2, 2); + + activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + addTarget(playerA, lion); + // 2 tokens, so stacking trigger. + setChoice(playerA, "At the beginning of combat on your turn, if a card was put " + + "into exile this turn, put a +1/+1 counter on this creature."); + + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, "Nightmare Token", 3, 3); + + setStopAt(7, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPowerToughness(playerA, "Nightmare Token", 3, 3); + } + + @Test + public void Ultimate() { + setStrictChooseMode(true); + + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + for (int i = 0; i < 10; ++i) { + // Alternating, so choices are possible on Ashiok's +1 + addCard(Zone.LIBRARY, playerA, lion); + addCard(Zone.LIBRARY, playerA, lurking); + } + + addCard(Zone.BATTLEFIELD, playerA, ashiok); + addCard(Zone.BATTLEFIELD, playerA, poet); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + addTarget(playerA, lion); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + addTarget(playerA, lurking); + + activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + addTarget(playerA, lurking); + + activateAbility(7, PhaseStep.PRECOMBAT_MAIN, playerA, "-7:", playerB); + + setStopAt(7, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertExileCount(playerB, 2 + 3 + 3); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/condition/common/WasCardExiledThisTurnCondition.java b/Mage/src/main/java/mage/abilities/condition/common/WasCardExiledThisTurnCondition.java new file mode 100644 index 00000000000..1d76e9bf40f --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/WasCardExiledThisTurnCondition.java @@ -0,0 +1,30 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.game.Game; +import mage.watchers.common.CardsExiledThisTurnWatcher; + +/** + * Checks if at least one card was put into exile this turn. + *

+ * /!\ Need the CardsExiledThisTurnWatcher to be set up. + * + * @author Susucr + */ +public enum WasCardExiledThisTurnCondition implements Condition { + + instance; + + @Override + public boolean apply(Game game, Ability source) { + CardsExiledThisTurnWatcher watcher = game.getState().getWatcher(CardsExiledThisTurnWatcher.class); + return watcher != null && watcher.getCountCardsExiledThisTurn() > 0; + } + + @Override + public String toString() { + return "a card was put into exile this turn"; + } + +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/TotalCardsExiledOwnedManaValue.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/TotalCardsExiledOwnedManaValue.java new file mode 100644 index 00000000000..400155b7708 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/TotalCardsExiledOwnedManaValue.java @@ -0,0 +1,52 @@ +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.Card; +import mage.game.Game; + +import java.util.List; + +public enum TotalCardsExiledOwnedManaValue implements DynamicValue { + instance; + + private static final Hint hint = new ValueHint("Total mana value of cards you own in exile", instance); + + private TotalCardsExiledOwnedManaValue() { + } + + @Override + public TotalCardsExiledOwnedManaValue copy() { + return this; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + int totalCMC = 0; + List cards = game.getExile().getAllCards( + game, + sourceAbility.getControllerId() + ); + for (Card card : cards) { + totalCMC += card.getManaValue(); + } + return totalCMC; + } + + @Override + public String getMessage() { + return "the total mana value of cards you own in exile"; + } + + @Override + public String toString() { + return "X"; + } + + public static Hint getHint() { + return hint; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/ExileCardsFromTopOfLibraryTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ExileCardsFromTopOfLibraryTargetEffect.java index e59e5defd0f..34948234ec5 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ExileCardsFromTopOfLibraryTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ExileCardsFromTopOfLibraryTargetEffect.java @@ -1,6 +1,9 @@ package mage.abilities.effects.common; import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.OneShotEffect; import mage.cards.Cards; import mage.cards.CardsImpl; @@ -11,29 +14,24 @@ import mage.players.Player; import mage.util.CardUtil; /** - * @author LevelX2 + * @author LevelX2, Susucr */ public class ExileCardsFromTopOfLibraryTargetEffect extends OneShotEffect { - int amount; - String targetName; + private final DynamicValue amount; public ExileCardsFromTopOfLibraryTargetEffect(int amount) { - this(amount, null); + this(StaticValue.get(amount)); } - public ExileCardsFromTopOfLibraryTargetEffect(int amount, String targetName) { + public ExileCardsFromTopOfLibraryTargetEffect(DynamicValue amount) { super(Outcome.Exile); - this.amount = amount; - this.staticText = (targetName == null ? "that player" : targetName) + " exiles the top " - + CardUtil.numberToText(amount, "") - + (amount == 1 ? "card" : " cards") + " of their library"; + this.amount = amount.copy(); } protected ExileCardsFromTopOfLibraryTargetEffect(final ExileCardsFromTopOfLibraryTargetEffect effect) { super(effect); - this.amount = effect.amount; - + this.amount = effect.amount.copy(); } @Override @@ -43,12 +41,24 @@ public class ExileCardsFromTopOfLibraryTargetEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { + int milled = amount.calculate(game, source, this); Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source)); - if (targetPlayer != null) { + if (milled > 0 && targetPlayer != null) { Cards cards = new CardsImpl(); - cards.addAllCards(targetPlayer.getLibrary().getTopCards(game, amount)); + cards.addAllCards(targetPlayer.getLibrary().getTopCards(game, milled)); return targetPlayer.moveCards(cards, Zone.EXILED, source, game); } return false; } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + return getTargetPointer().describeTargets(mode.getTargets(), "that player") + + " exiles the top " + + (amount.toString().equals("1") ? "card" : CardUtil.numberToText(amount.toString(), "a") + " cards") + + " of their library"; + } } diff --git a/Mage/src/main/java/mage/game/Exile.java b/Mage/src/main/java/mage/game/Exile.java index 3ef16b60f41..5d729bfa537 100644 --- a/Mage/src/main/java/mage/game/Exile.java +++ b/Mage/src/main/java/mage/game/Exile.java @@ -76,7 +76,7 @@ public class Exile implements Serializable, Copyable { } /** - * Return exiled cards from specific player. Use it in effects to find all cards in range. + * Return exiled cards owned by a specific player. Use it in effects to find all cards in range. * * @param game * @param fromPlayerId diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 572b93cf243..bb51faeaba0 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -331,7 +331,7 @@ public class GameEvent implements Serializable { PLANESWALK, PLANESWALKED, PAID_CUMULATIVE_UPKEEP, DIDNT_PAY_CUMULATIVE_UPKEEP, - LIFE_PAID, + PAY_LIFE, LIFE_PAID, CASCADE_LAND, LEARN, //permanent events diff --git a/Mage/src/main/java/mage/game/permanent/token/AshiokWickedManipulatorNightmareToken.java b/Mage/src/main/java/mage/game/permanent/token/AshiokWickedManipulatorNightmareToken.java new file mode 100644 index 00000000000..842eb877d58 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/AshiokWickedManipulatorNightmareToken.java @@ -0,0 +1,52 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.common.BeginningOfCombatTriggeredAbility; +import mage.abilities.condition.common.WasCardExiledThisTurnCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.counters.CounterType; + +/** + * @author Susucr + */ +public final class AshiokWickedManipulatorNightmareToken extends TokenImpl { + + private static final Hint hint = new ConditionHint(WasCardExiledThisTurnCondition.instance); + + /** + * /!\ You need to add CardsExiledThisTurnWatcher to any card using this token + */ + public AshiokWickedManipulatorNightmareToken() { + super("Nightmare Token", "1/1 black Nightmare creature tokens with \"At the beginning of combat on your turn, if a card was put into exile this turn, put a +1/+1 counter on this creature.\""); + cardType.add(CardType.CREATURE); + color.setBlack(true); + subtype.add(SubType.NIGHTMARE); + power = new MageInt(1); + toughness = new MageInt(1); + + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new BeginningOfCombatTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), + TargetController.YOU, + false + ), + WasCardExiledThisTurnCondition.instance, + "At the beginning of combat on your turn, if a card was put into exile " + + "this turn, put a +1/+1 counter on this creature." + ).addHint(hint)); + } + + private AshiokWickedManipulatorNightmareToken(final AshiokWickedManipulatorNightmareToken token) { + super(token); + } + + public AshiokWickedManipulatorNightmareToken copy() { + return new AshiokWickedManipulatorNightmareToken(this); + } +} diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index fa6a5c61052..a83d8327ae9 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -1496,6 +1496,11 @@ public final class CardUtil { return false; } + if (game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.PAY_LIFE, player.getId(), source, player.getId(), lifeToPay))) { + // 2023-08-20: For now, Cost being replaced are paid. + // Waiting on actual ruling of Ashiok, Wicked Manipulator. + return true; + } if (player.loseLife(lifeToPay, game, source, false) >= lifeToPay) { game.fireEvent(GameEvent.getEvent(GameEvent.EventType.LIFE_PAID, player.getId(), source, player.getId(), lifeToPay)); return true; diff --git a/Mage/src/main/java/mage/watchers/common/CardsExiledThisTurnWatcher.java b/Mage/src/main/java/mage/watchers/common/CardsExiledThisTurnWatcher.java new file mode 100644 index 00000000000..783eb3bf0c0 --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/CardsExiledThisTurnWatcher.java @@ -0,0 +1,38 @@ +package mage.watchers.common; + +import mage.constants.WatcherScope; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.watchers.Watcher; + +/** + * @author Susucr + */ +public class CardsExiledThisTurnWatcher extends Watcher { + + private int countExiled = 0; + + public CardsExiledThisTurnWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.ZONE_CHANGE + && ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) { + countExiled++; + } + } + + public int getCountCardsExiledThisTurn() { + return countExiled; + } + + @Override + public void reset() { + super.reset(); + countExiled = 0; + } +}