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
This commit is contained in:
Evan Kranzler 2022-08-27 14:53:39 -04:00 committed by GitHub
parent e557b7a04b
commit 90bd0dbf63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 300 additions and 7 deletions

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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("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("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("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 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("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("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)); cards.add(new SetCardInfo("Tidepool Turtle", 69, Rarity.COMMON, mage.cards.t.TidepoolTurtle.class));

View file

@ -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);
}
}

View file

@ -6,8 +6,9 @@ import mage.abilities.TriggeredAbility;
import mage.abilities.TriggeredAbilityImpl; import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects; import mage.abilities.effects.Effects;
import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.OneShotEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.constants.Outcome;
import mage.constants.SagaChapter; import mage.constants.SagaChapter;
import mage.constants.Zone; import mage.constants.Zone;
import mage.counters.CounterType; import mage.counters.CounterType;
@ -16,6 +17,7 @@ import mage.game.events.GameEvent;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.stack.StackAbility; import mage.game.stack.StackAbility;
import mage.game.stack.StackObject; import mage.game.stack.StackObject;
import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.Targets; import mage.target.Targets;
import mage.util.CardUtil; import mage.util.CardUtil;
@ -29,18 +31,24 @@ public class SagaAbility extends SimpleStaticAbility {
private final SagaChapter maxChapter; private final SagaChapter maxChapter;
private final boolean showSacText; private final boolean showSacText;
private final boolean readAhead;
public SagaAbility(Card card) { public SagaAbility(Card card) {
this(card, SagaChapter.CHAPTER_III); this(card, SagaChapter.CHAPTER_III);
} }
public SagaAbility(Card card, SagaChapter maxChapter) { 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.maxChapter = maxChapter;
this.showSacText = card.getSecondCardFace() == null; this.showSacText = card.getSecondCardFace() == null;
this.readAhead = readAhead;
this.setRuleVisible(true); this.setRuleVisible(true);
this.setRuleAtTheTop(true); this.setRuleAtTheTop(true);
Ability ability = new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.LORE.createInstance())); Ability ability = new EntersBattlefieldAbility(new SagaLoreCountersEffect(readAhead, maxChapter));
ability.setRuleVisible(false); ability.setRuleVisible(false);
card.addAbility(ability); card.addAbility(ability);
} }
@ -49,6 +57,7 @@ public class SagaAbility extends SimpleStaticAbility {
super(ability); super(ability);
this.maxChapter = ability.maxChapter; this.maxChapter = ability.maxChapter;
this.showSacText = ability.showSacText; this.showSacText = ability.showSacText;
this.readAhead = ability.readAhead;
} }
public void addChapterEffect(Card card, SagaChapter chapter, Effect... effects) { 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) { 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++) { 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) { for (Effect effect : effects) {
if (effect != null) { if (effect != null) {
ability.addEffect(effect.copy()); ability.addEffect(effect.copy());
@ -108,7 +117,10 @@ public class SagaAbility extends SimpleStaticAbility {
@Override @Override
public String getRule() { public String getRule() {
return "<i>(As this Saga enters and after your draw step, add a lore counter." return (readAhead
? "Read ahead <i>(Choose a chapter and start with that many lore counters. " +
"Add one after your draw step. Skipped chapters don't trigger."
: "<i>(As this Saga enters and after your draw step, add a lore counter.")
+ (showSacText ? " Sacrifice after " + maxChapter.toString() + '.' : "") + ")</i> "; + (showSacText ? " Sacrifice after " + maxChapter.toString() + '.' : "") + ")</i> ";
} }
@ -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 { class ChapterTriggeredAbility extends TriggeredAbilityImpl {
private final SagaChapter chapterFrom, chapterTo; 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); super(Zone.ALL, effect, optional);
this.chapterFrom = chapterFrom; this.chapterFrom = chapterFrom;
this.chapterTo = chapterTo; this.chapterTo = chapterTo;
this.readAhead = readAhead;
} }
public ChapterTriggeredAbility(final ChapterTriggeredAbility ability) { public ChapterTriggeredAbility(final ChapterTriggeredAbility ability) {
super(ability); super(ability);
this.chapterFrom = ability.chapterFrom; this.chapterFrom = ability.chapterFrom;
this.chapterTo = ability.chapterTo; this.chapterTo = ability.chapterTo;
this.readAhead = ability.readAhead;
} }
@Override @Override
public boolean checkEventType(GameEvent event, Game game) { public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.COUNTER_ADDED; return event.getType() == GameEvent.EventType.COUNTERS_ADDED;
} }
@Override @Override
@ -161,6 +219,9 @@ class ChapterTriggeredAbility extends TriggeredAbilityImpl {
return false; return false;
} }
int loreCounters = permanent.getCounters(game).getCount(CounterType.LORE); int loreCounters = permanent.getCounters(game).getCount(CounterType.LORE);
if (readAhead && permanent.getTurnsOnBattlefield() == 0) {
return chapterFrom.getNumber() == loreCounters;
}
return loreCounters - event.getAmount() < chapterFrom.getNumber() return loreCounters - event.getAmount() < chapterFrom.getNumber()
&& chapterFrom.getNumber() <= loreCounters; && chapterFrom.getNumber() <= loreCounters;
} }