From 90bd0dbf6333aef1dd1ce39fe57bf1cd1cc7bfe6 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 27 Aug 2022 14:53:39 -0400 Subject: [PATCH] Implement Read Ahead mechanic (#9407) * implement Read Ahead mechanic * [DMU] Implemented The World Spell * [DMU] Implemented The Elder Dragon War * added read ahead test * fix verify failure * small change to test * fix read ahead text --- .../src/mage/cards/t/TheElderDragonWar.java | 61 ++++++++++++ Mage.Sets/src/mage/cards/t/TheWorldSpell.java | 97 +++++++++++++++++++ Mage.Sets/src/mage/sets/DominariaUnited.java | 2 + .../cards/enchantments/ReadAheadTest.java | 72 ++++++++++++++ .../mage/abilities/common/SagaAbility.java | 75 ++++++++++++-- 5 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TheElderDragonWar.java create mode 100644 Mage.Sets/src/mage/cards/t/TheWorldSpell.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/enchantments/ReadAheadTest.java diff --git a/Mage.Sets/src/mage/cards/t/TheElderDragonWar.java b/Mage.Sets/src/mage/cards/t/TheElderDragonWar.java new file mode 100644 index 0000000000..61ffb9d190 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheElderDragonWar.java @@ -0,0 +1,61 @@ +package mage.cards.t; + +import mage.abilities.common.SagaAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DamageAllEffect; +import mage.abilities.effects.common.DamagePlayersEffect; +import mage.abilities.effects.common.discard.DiscardAndDrawThatManyEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SagaChapter; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.filter.StaticFilters; +import mage.game.permanent.token.DragonToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TheElderDragonWar extends CardImpl { + + public TheElderDragonWar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{R}{R}"); + + this.subtype.add(SubType.SAGA); + + // Read ahead + SagaAbility sagaAbility = new SagaAbility(this, SagaChapter.CHAPTER_III, true); + + // I -- The Elder Dragon War deals 2 damage to each creature and each opponent. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_I, + new DamageAllEffect(2, StaticFilters.FILTER_PERMANENT_CREATURE), + new DamagePlayersEffect(2, TargetController.OPPONENT).setText("and each opponent") + ); + + // II -- Discard any number of cards, then draw that many cards. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_II, + new DiscardAndDrawThatManyEffect(Integer.MAX_VALUE) + ); + + // III -- Create a 4/4 red Dragon creature token with flying. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_III, + new CreateTokenEffect(new DragonToken()) + ); + this.addAbility(sagaAbility); + } + + private TheElderDragonWar(final TheElderDragonWar card) { + super(card); + } + + @Override + public TheElderDragonWar copy() { + return new TheElderDragonWar(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TheWorldSpell.java b/Mage.Sets/src/mage/cards/t/TheWorldSpell.java new file mode 100644 index 0000000000..1a80a6965f --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheWorldSpell.java @@ -0,0 +1,97 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.common.SagaAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.abilities.effects.common.LookLibraryControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.common.FilterPermanentCard; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInHand; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TheWorldSpell extends CardImpl { + + private static final FilterCard filter = new FilterPermanentCard("non-Saga permanent card"); + + static { + filter.add(Predicates.not(SubType.SAGA.getPredicate())); + } + + public TheWorldSpell(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{5}{G}{G}"); + + this.subtype.add(SubType.SAGA); + + // Read ahead + SagaAbility sagaAbility = new SagaAbility(this, SagaChapter.CHAPTER_III, true); + + // I, II -- Look at the top seven cards of your library. You may reveal a non-Saga permanent card from among them and put it into your hand. Put the rest on the bottom of your library in a random order. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_I, SagaChapter.CHAPTER_II, + new LookLibraryAndPickControllerEffect( + 7, 1, filter, + LookLibraryControllerEffect.PutCards.HAND, + LookLibraryControllerEffect.PutCards.BOTTOM_RANDOM + ) + ); + + // III -- Put up to two non-Saga permanent cards from your hand onto the battlefield. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_III, new TheWorldSpellEffect()); + this.addAbility(sagaAbility); + } + + private TheWorldSpell(final TheWorldSpell card) { + super(card); + } + + @Override + public TheWorldSpell copy() { + return new TheWorldSpell(this); + } +} + +class TheWorldSpellEffect extends OneShotEffect { + + private static final FilterCard filter = new FilterPermanentCard("non-Saga permanent cards"); + + static { + filter.add(Predicates.not(SubType.SAGA.getPredicate())); + } + + TheWorldSpellEffect() { + super(Outcome.Benefit); + staticText = "put up to two non-Saga permanent cards from your hand onto the battlefield"; + } + + private TheWorldSpellEffect(final TheWorldSpellEffect effect) { + super(effect); + } + + @Override + public TheWorldSpellEffect copy() { + return new TheWorldSpellEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetCardInHand target = new TargetCardInHand(0, 2, filter); + player.choose(outcome, player.getHand(), target, game); + return player.moveCards(new CardsImpl(target.getTargets()), Zone.BATTLEFIELD, source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/DominariaUnited.java b/Mage.Sets/src/mage/sets/DominariaUnited.java index a5763ee386..74d57bc313 100644 --- a/Mage.Sets/src/mage/sets/DominariaUnited.java +++ b/Mage.Sets/src/mage/sets/DominariaUnited.java @@ -189,7 +189,9 @@ public final class DominariaUnited extends ExpansionSet { cards.add(new SetCardInfo("Tear Asunder", 183, Rarity.UNCOMMON, mage.cards.t.TearAsunder.class)); cards.add(new SetCardInfo("Temporal Firestorm", 147, Rarity.RARE, mage.cards.t.TemporalFirestorm.class)); cards.add(new SetCardInfo("Territorial Maro", 184, Rarity.UNCOMMON, mage.cards.t.TerritorialMaro.class)); + cards.add(new SetCardInfo("The Elder Dragon War", 121, Rarity.RARE, mage.cards.t.TheElderDragonWar.class)); cards.add(new SetCardInfo("The Raven Man", 103, Rarity.RARE, mage.cards.t.TheRavenMan.class)); + cards.add(new SetCardInfo("The World Spell", 189, Rarity.MYTHIC, mage.cards.t.TheWorldSpell.class)); cards.add(new SetCardInfo("Threats Undetected", 185, Rarity.RARE, mage.cards.t.ThreatsUndetected.class)); cards.add(new SetCardInfo("Thrill of Possibility", 148, Rarity.COMMON, mage.cards.t.ThrillOfPossibility.class)); cards.add(new SetCardInfo("Tidepool Turtle", 69, Rarity.COMMON, mage.cards.t.TidepoolTurtle.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/ReadAheadTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/ReadAheadTest.java new file mode 100644 index 0000000000..b87cf4f45f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/ReadAheadTest.java @@ -0,0 +1,72 @@ +package org.mage.test.cards.enchantments; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class ReadAheadTest extends CardTestPlayerBase { + + private static final String war = "The Elder Dragon War"; + private static final String recall = "Ancestral Recall"; + + @Test + public void testElderDragonWarChapter1() { + addCard(Zone.HAND, playerA, war); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, war); + setChoice(playerA, "X=1"); + + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerB, 20 - 2); + assertPermanentCount(playerA, war, 1); + assertGraveyardCount(playerA, war, 0); + } + + @Test + public void testElderDragonWarChapter2() { + addCard(Zone.HAND, playerA, war); + addCard(Zone.HAND, playerA, recall); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, war); + setChoice(playerA, "X=2"); + setChoice(playerA, recall); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerB, 20); + assertGraveyardCount(playerA, recall, 1); + assertHandCount(playerA, recall, 0); + assertPermanentCount(playerA, war, 1); + assertGraveyardCount(playerA, war, 0); + } + + @Test + public void testElderDragonWarChapter3() { + addCard(Zone.HAND, playerA, war); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, war); + setChoice(playerA, "X=3"); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertLife(playerB, 20); + assertPermanentCount(playerA, "Dragon Token", 1); + assertPermanentCount(playerA, war, 0); + assertGraveyardCount(playerA, war, 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/SagaAbility.java b/Mage/src/main/java/mage/abilities/common/SagaAbility.java index 2287d992f9..e520484348 100644 --- a/Mage/src/main/java/mage/abilities/common/SagaAbility.java +++ b/Mage/src/main/java/mage/abilities/common/SagaAbility.java @@ -6,8 +6,9 @@ import mage.abilities.TriggeredAbility; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; -import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.OneShotEffect; import mage.cards.Card; +import mage.constants.Outcome; import mage.constants.SagaChapter; import mage.constants.Zone; import mage.counters.CounterType; @@ -16,6 +17,7 @@ import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.stack.StackAbility; import mage.game.stack.StackObject; +import mage.players.Player; import mage.target.Target; import mage.target.Targets; import mage.util.CardUtil; @@ -29,18 +31,24 @@ public class SagaAbility extends SimpleStaticAbility { private final SagaChapter maxChapter; private final boolean showSacText; + private final boolean readAhead; public SagaAbility(Card card) { this(card, SagaChapter.CHAPTER_III); } public SagaAbility(Card card, SagaChapter maxChapter) { - super(Zone.ALL, new AddCountersSourceEffect(CounterType.LORE.createInstance())); + this(card, maxChapter, false); + } + + public SagaAbility(Card card, SagaChapter maxChapter, boolean readAhead) { + super(Zone.ALL, null); this.maxChapter = maxChapter; this.showSacText = card.getSecondCardFace() == null; + this.readAhead = readAhead; this.setRuleVisible(true); this.setRuleAtTheTop(true); - Ability ability = new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.LORE.createInstance())); + Ability ability = new EntersBattlefieldAbility(new SagaLoreCountersEffect(readAhead, maxChapter)); ability.setRuleVisible(false); card.addAbility(ability); } @@ -49,6 +57,7 @@ public class SagaAbility extends SimpleStaticAbility { super(ability); this.maxChapter = ability.maxChapter; this.showSacText = ability.showSacText; + this.readAhead = ability.readAhead; } public void addChapterEffect(Card card, SagaChapter chapter, Effect... effects) { @@ -81,7 +90,7 @@ public class SagaAbility extends SimpleStaticAbility { public void addChapterEffect(Card card, SagaChapter fromChapter, SagaChapter toChapter, Effects effects, Targets targets, boolean optional, Mode... modes) { for (int i = fromChapter.getNumber(); i <= toChapter.getNumber(); i++) { - ChapterTriggeredAbility ability = new ChapterTriggeredAbility(null, SagaChapter.getChapter(i), toChapter, optional); + ChapterTriggeredAbility ability = new ChapterTriggeredAbility(null, SagaChapter.getChapter(i), toChapter, optional, readAhead); for (Effect effect : effects) { if (effect != null) { ability.addEffect(effect.copy()); @@ -108,7 +117,10 @@ public class SagaAbility extends SimpleStaticAbility { @Override public String getRule() { - return "(As this Saga enters and after your draw step, add a lore counter." + return (readAhead + ? "Read ahead (Choose a chapter and start with that many lore counters. " + + "Add one after your draw step. Skipped chapters don't trigger." + : "(As this Saga enters and after your draw step, add a lore counter.") + (showSacText ? " Sacrifice after " + maxChapter.toString() + '.' : "") + ") "; } @@ -129,25 +141,71 @@ public class SagaAbility extends SimpleStaticAbility { } } +class SagaLoreCountersEffect extends OneShotEffect { + + private final boolean readAhead; + private final SagaChapter maxChapter; + + SagaLoreCountersEffect(boolean readAhead, SagaChapter maxChapter) { + super(Outcome.Benefit); + this.readAhead = readAhead; + this.maxChapter = maxChapter; + } + + private SagaLoreCountersEffect(final SagaLoreCountersEffect effect) { + super(effect); + this.readAhead = effect.readAhead; + this.maxChapter = effect.maxChapter; + } + + @Override + public SagaLoreCountersEffect copy() { + return new SagaLoreCountersEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanentEntering(source.getSourceId()); + if (permanent == null) { + return false; + } + if (!readAhead) { + return permanent.addCounters(CounterType.LORE.createInstance(), source, game); + } + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + int counters = player.getAmount( + 1, maxChapter.getNumber(), + "Choose the number of lore counters to enter with", game + ); + return permanent.addCounters(CounterType.LORE.createInstance(counters), source, game); + } +} + class ChapterTriggeredAbility extends TriggeredAbilityImpl { private final SagaChapter chapterFrom, chapterTo; + private final boolean readAhead; - public ChapterTriggeredAbility(Effect effect, SagaChapter chapterFrom, SagaChapter chapterTo, boolean optional) { + public ChapterTriggeredAbility(Effect effect, SagaChapter chapterFrom, SagaChapter chapterTo, boolean optional, boolean readAhead) { super(Zone.ALL, effect, optional); this.chapterFrom = chapterFrom; this.chapterTo = chapterTo; + this.readAhead = readAhead; } public ChapterTriggeredAbility(final ChapterTriggeredAbility ability) { super(ability); this.chapterFrom = ability.chapterFrom; this.chapterTo = ability.chapterTo; + this.readAhead = ability.readAhead; } @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.COUNTER_ADDED; + return event.getType() == GameEvent.EventType.COUNTERS_ADDED; } @Override @@ -161,6 +219,9 @@ class ChapterTriggeredAbility extends TriggeredAbilityImpl { return false; } int loreCounters = permanent.getCounters(game).getCount(CounterType.LORE); + if (readAhead && permanent.getTurnsOnBattlefield() == 0) { + return chapterFrom.getNumber() == loreCounters; + } return loreCounters - event.getAmount() < chapterFrom.getNumber() && chapterFrom.getNumber() <= loreCounters; }