mirror of
https://github.com/magefree/mage.git
synced 2026-01-26 21:29:17 -08:00
[LCC] Implement Topography Tracker (#11504)
* Refactor replacement effects on ExploreEvent by using a queue of events
This commit is contained in:
parent
15fcc22bb3
commit
5c83818cba
6 changed files with 289 additions and 37 deletions
84
Mage.Sets/src/mage/cards/t/TopographyTracker.java
Normal file
84
Mage.Sets/src/mage/cards/t/TopographyTracker.java
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package mage.cards.t;
|
||||
|
||||
import java.util.UUID;
|
||||
import mage.MageInt;
|
||||
import mage.abilities.Ability;
|
||||
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
|
||||
import mage.abilities.common.SimpleStaticAbility;
|
||||
import mage.abilities.effects.ReplacementEffectImpl;
|
||||
import mage.abilities.effects.common.CreateTokenEffect;
|
||||
import mage.constants.Duration;
|
||||
import mage.constants.Outcome;
|
||||
import mage.constants.SubType;
|
||||
import mage.cards.CardImpl;
|
||||
import mage.cards.CardSetInfo;
|
||||
import mage.constants.CardType;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ExploreEvent;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.permanent.token.MapToken;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Grath
|
||||
*/
|
||||
public final class TopographyTracker extends CardImpl {
|
||||
|
||||
public TopographyTracker(UUID ownerId, CardSetInfo setInfo) {
|
||||
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}");
|
||||
|
||||
this.subtype.add(SubType.MERFOLK);
|
||||
this.subtype.add(SubType.SCOUT);
|
||||
this.power = new MageInt(2);
|
||||
this.toughness = new MageInt(2);
|
||||
|
||||
// When Topography Tracker enters the battlefield, create a Map token.
|
||||
this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new MapToken()), false));
|
||||
|
||||
// If a creature you control would explore, instead it explores, then it explores again.
|
||||
this.addAbility(new SimpleStaticAbility(new TopographyTrackerEffect()));
|
||||
}
|
||||
|
||||
private TopographyTracker(final TopographyTracker card) {
|
||||
super(card);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TopographyTracker copy() {
|
||||
return new TopographyTracker(this);
|
||||
}
|
||||
}
|
||||
|
||||
class TopographyTrackerEffect extends ReplacementEffectImpl {
|
||||
|
||||
TopographyTrackerEffect() {
|
||||
super(Duration.WhileOnBattlefield, Outcome.Benefit);
|
||||
staticText = "If a creature you control would explore, instead it explores, then it explores again.";
|
||||
}
|
||||
|
||||
private TopographyTrackerEffect(final TopographyTrackerEffect effect) {
|
||||
super(effect);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TopographyTrackerEffect copy() {
|
||||
return new TopographyTrackerEffect(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checksEventType(GameEvent event, Game game) {
|
||||
return event.getType() == GameEvent.EventType.EXPLORE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean applies(GameEvent event, Ability source, Game game) {
|
||||
return game.getPermanent(event.getTargetId()).isControlledBy(event.getPlayerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
|
||||
ExploreEvent exploreEvent = (ExploreEvent)event;
|
||||
exploreEvent.doubleExplores();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,8 @@ import mage.constants.Duration;
|
|||
import mage.constants.Outcome;
|
||||
import mage.filter.StaticFilters;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ExploreEvent;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.players.Player;
|
||||
import mage.target.common.TargetControlledCreaturePermanent;
|
||||
|
||||
import java.util.UUID;
|
||||
|
|
@ -89,8 +89,8 @@ class TwistsAndTurnsReplacementEffect extends ReplacementEffectImpl {
|
|||
|
||||
@Override
|
||||
public boolean replaceEvent(GameEvent event, Ability source, Game game) {
|
||||
Player controller = game.getPlayer(source.getControllerId());
|
||||
controller.scry(1, source, game);
|
||||
ExploreEvent exploreEvent = (ExploreEvent)event;
|
||||
exploreEvent.addScry();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,6 +270,7 @@ public final class LostCavernsOfIxalanCommander extends ExpansionSet {
|
|||
cards.add(new SetCardInfo("Timothar, Baron of Bats", 210, Rarity.MYTHIC, mage.cards.t.TimotharBaronOfBats.class));
|
||||
cards.add(new SetCardInfo("Tishana, Voice of Thunder", 291, Rarity.MYTHIC, mage.cards.t.TishanaVoiceOfThunder.class));
|
||||
cards.add(new SetCardInfo("Topiary Stomper", 261, Rarity.RARE, mage.cards.t.TopiaryStomper.class));
|
||||
cards.add(new SetCardInfo("Topography Tracker", 95, Rarity.RARE, mage.cards.t.TopographyTracker.class));
|
||||
cards.add(new SetCardInfo("Tributary Instructor", 96, Rarity.RARE, mage.cards.t.TributaryInstructor.class));
|
||||
cards.add(new SetCardInfo("Twilight Prophet", 211, Rarity.MYTHIC, mage.cards.t.TwilightProphet.class));
|
||||
cards.add(new SetCardInfo("Unclaimed Territory", 366, Rarity.UNCOMMON, mage.cards.u.UnclaimedTerritory.class));
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ public class ExploreTest extends CardTestPlayerBase {
|
|||
private static final String enter = "Enter the Unknown"; // G Sorcery - Target creature you control explores.
|
||||
private static final String quicksand = "Quicksand"; // Land
|
||||
private static final String gg = "Giant Growth"; // Nonland
|
||||
private static final String twists = "Twists and Turns"; // Replace Explore with Scry 1, Explore
|
||||
private static final String topography = "Topography Tracker"; // Replace Explore with Explore, Explore
|
||||
|
||||
|
||||
@Test
|
||||
|
|
@ -242,10 +244,8 @@ public class ExploreTest extends CardTestPlayerBase {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void exploreReplacement() {
|
||||
String twists = "Twists and Turns";
|
||||
public void exploreReplacementScry() {
|
||||
// If a creature you control would explore, instead you scry 1, then that creature explores.
|
||||
|
||||
addCard(Zone.BATTLEFIELD, playerA, ww);
|
||||
addCard(Zone.BATTLEFIELD, playerA, twists);
|
||||
addCard(Zone.HAND, playerA, mb);
|
||||
|
|
@ -268,5 +268,104 @@ public class ExploreTest extends CardTestPlayerBase {
|
|||
assertGraveyardCount(playerA, 0);
|
||||
assertLibraryCount(playerA, 0);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void exploreReplacementTwice() {
|
||||
// If a creature you control would explore, instead it explores, then it explores again.
|
||||
addCard(Zone.BATTLEFIELD, playerA, ww);
|
||||
addCard(Zone.BATTLEFIELD, playerA, topography);
|
||||
addCard(Zone.HAND, playerA, mb);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
removeAllCardsFromLibrary(playerA);
|
||||
skipInitShuffling();
|
||||
addCard(Zone.LIBRARY, playerA, quicksand, 2);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mb);
|
||||
setChoice(playerA, "Whenever a creature you control explores"); // order trigger
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
|
||||
assertCounterCount(mb, CounterType.P1P1, 0);
|
||||
assertCounterCount(ww, CounterType.P1P1, 2);
|
||||
assertLife(playerA, 26);
|
||||
assertHandCount(playerA, quicksand, 2);
|
||||
assertGraveyardCount(playerA, 0);
|
||||
assertLibraryCount(playerA, 0);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void exploreReplacementScryOnce() {
|
||||
String flamespeaker = "Flamespeaker Adept";
|
||||
// If a creature you control would explore, instead you scry 1, then that creature explores.
|
||||
// If a creature you control would explore, instead it explores, then it explores again.
|
||||
addCard(Zone.BATTLEFIELD, playerA, ww);
|
||||
addCard(Zone.BATTLEFIELD, playerA, twists);
|
||||
addCard(Zone.BATTLEFIELD, playerA, topography);
|
||||
addCard(Zone.BATTLEFIELD, playerA, flamespeaker);
|
||||
addCard(Zone.HAND, playerA, mb);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
removeAllCardsFromLibrary(playerA);
|
||||
skipInitShuffling();
|
||||
addCard(Zone.LIBRARY, playerA, quicksand, 2);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mb);
|
||||
// Order Twists and Turns replacement effect first
|
||||
setChoice(playerA, twists);
|
||||
addTarget(playerA, TestPlayer.TARGET_SKIP); // scry to top (no targets to bottom)
|
||||
setChoice(playerA, "Whenever a creature you control explores"); // order trigger
|
||||
setChoice(playerA, "Whenever a creature you control explores"); // order trigger
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
|
||||
assertCounterCount(mb, CounterType.P1P1, 0);
|
||||
assertCounterCount(ww, CounterType.P1P1, 2);
|
||||
assertPowerToughness(playerA, flamespeaker, 4,3);
|
||||
assertLife(playerA, 26);
|
||||
assertHandCount(playerA, quicksand, 2);
|
||||
assertGraveyardCount(playerA, 0);
|
||||
assertLibraryCount(playerA, 0);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void exploreReplacementScryTwice() {
|
||||
String flamespeaker = "Flamespeaker Adept";
|
||||
// If a creature you control would explore, instead it explores, then it explores again.
|
||||
// If a creature you control would explore, instead you scry 1, then that creature explores.
|
||||
addCard(Zone.BATTLEFIELD, playerA, ww);
|
||||
addCard(Zone.BATTLEFIELD, playerA, twists);
|
||||
addCard(Zone.BATTLEFIELD, playerA, topography);
|
||||
addCard(Zone.BATTLEFIELD, playerA, flamespeaker);
|
||||
addCard(Zone.HAND, playerA, mb);
|
||||
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
|
||||
removeAllCardsFromLibrary(playerA);
|
||||
skipInitShuffling();
|
||||
addCard(Zone.LIBRARY, playerA, quicksand, 2);
|
||||
|
||||
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, mb);
|
||||
// Order Topography Tracker replacement effect first
|
||||
setChoice(playerA, topography);
|
||||
addTarget(playerA, TestPlayer.TARGET_SKIP); // scry to top (no targets to bottom)
|
||||
addTarget(playerA, TestPlayer.TARGET_SKIP); // scry to top again (no targets to bottom)
|
||||
setChoice(playerA, "Whenever a creature you control explores"); // order trigger
|
||||
setChoice(playerA, "Whenever a creature you control explores"); // order trigger
|
||||
setChoice(playerA, "Whenever you scry"); // order trigger
|
||||
|
||||
setStopAt(1, PhaseStep.BEGIN_COMBAT);
|
||||
setStrictChooseMode(true);
|
||||
execute();
|
||||
|
||||
assertCounterCount(mb, CounterType.P1P1, 0);
|
||||
assertCounterCount(ww, CounterType.P1P1, 2);
|
||||
assertPowerToughness(playerA, flamespeaker, 6,3);
|
||||
assertLife(playerA, 26);
|
||||
assertHandCount(playerA, quicksand, 2);
|
||||
assertGraveyardCount(playerA, 0);
|
||||
assertLibraryCount(playerA, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import mage.constants.Outcome;
|
|||
import mage.constants.Zone;
|
||||
import mage.counters.CounterType;
|
||||
import mage.game.Game;
|
||||
import mage.game.events.ExploreEvent;
|
||||
import mage.game.events.ExploredEvent;
|
||||
import mage.game.events.GameEvent;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
|
@ -70,41 +71,57 @@ public class ExploreSourceEffect extends OneShotEffect {
|
|||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < amount; ++i) {
|
||||
GameEvent event = new GameEvent(GameEvent.EventType.EXPLORE, permanentId, source, permanent.getControllerId());
|
||||
if (game.replaceEvent(event)) {
|
||||
continue;
|
||||
// Because of Topography Tracker and Twists and Turns, we need to be able to have a mix of Explores and Scry 1s
|
||||
// The ExploreEvent generates with a queue of EventType.EXPLORE equal to amount, the replacement effects
|
||||
// can then replace each EXPLORE with either two EXPLOREs or a SCRY and an EXPLORE.
|
||||
ExploreEvent event = new ExploreEvent(permanent, source, amount);
|
||||
if (game.replaceEvent(event)) {
|
||||
return false;
|
||||
}
|
||||
for (GameEvent.EventType eventType : event.getEventQueue()) {
|
||||
switch (eventType) {
|
||||
case SCRY:
|
||||
controller.scry(1, source, game);
|
||||
break;
|
||||
case EXPLORE:
|
||||
doOneExplore(game, permanent, source, controller);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Wrong code usage: unrecognized event type in explore event");
|
||||
}
|
||||
// 701.40a Certain abilities instruct a permanent to explore. To do so, that permanent’s controller reveals
|
||||
// the top card of their library. If a land card is revealed this way, that player puts that card into their
|
||||
// hand. Otherwise, that player puts a +1/+1 counter on the exploring permanent and may put the revealed
|
||||
// card into their graveyard.
|
||||
Card card = controller.getLibrary().getFromTop(game);
|
||||
if (card != null) {
|
||||
controller.revealCards("Explored card", new CardsImpl(card), game);
|
||||
if (card.isLand(game)) {
|
||||
controller.moveCards(card, Zone.HAND, source, game);
|
||||
} else {
|
||||
addCounter(game, permanent, source);
|
||||
if (controller.chooseUse(Outcome.Neutral, "Put " + card.getLogName() + " in your graveyard?", source, game)) {
|
||||
controller.moveCards(card, Zone.GRAVEYARD, source, game);
|
||||
} else {
|
||||
game.informPlayers(controller.getLogName() + " leaves " + card.getLogName() + " on top of their library.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card is revealed, most likely because that player's library is empty,
|
||||
// the exploring creature receives a +1/+1 counter.
|
||||
addCounter(game, permanent, source);
|
||||
}
|
||||
game.getState().processAction(game);
|
||||
// 701.40b A permanent “explores” after the process described in rule 701.40a is complete, even if some or all of
|
||||
// those actions were impossible.
|
||||
game.fireEvent(new ExploredEvent(permanent, source, card));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void doOneExplore(Game game, Permanent permanent, Ability source, Player controller) {
|
||||
// 701.40a Certain abilities instruct a permanent to explore. To do so, that permanent’s controller reveals
|
||||
// the top card of their library. If a land card is revealed this way, that player puts that card into their
|
||||
// hand. Otherwise, that player puts a +1/+1 counter on the exploring permanent and may put the revealed
|
||||
// card into their graveyard.
|
||||
Card card = controller.getLibrary().getFromTop(game);
|
||||
if (card != null) {
|
||||
controller.revealCards("Explored card", new CardsImpl(card), game);
|
||||
if (card.isLand(game)) {
|
||||
controller.moveCards(card, Zone.HAND, source, game);
|
||||
} else {
|
||||
addCounter(game, permanent, source);
|
||||
if (controller.chooseUse(Outcome.Neutral, "Put " + card.getLogName() + " in your graveyard?", source, game)) {
|
||||
controller.moveCards(card, Zone.GRAVEYARD, source, game);
|
||||
} else {
|
||||
game.informPlayers(controller.getLogName() + " leaves " + card.getLogName() + " on top of their library.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If no card is revealed, most likely because that player's library is empty,
|
||||
// the exploring creature receives a +1/+1 counter.
|
||||
addCounter(game, permanent, source);
|
||||
}
|
||||
game.getState().processAction(game);
|
||||
// 701.40b A permanent “explores” after the process described in rule 701.40a is complete, even if some or all of
|
||||
// those actions were impossible.
|
||||
game.fireEvent(new ExploredEvent(permanent, source, card));
|
||||
}
|
||||
|
||||
private static void addCounter(Game game, Permanent permanent, Ability source) {
|
||||
if (game.getState().getZone(permanent.getId()) == Zone.BATTLEFIELD) { // needed in case LKI object is used
|
||||
permanent.addCounters(CounterType.P1P1.createInstance(), source.getControllerId(), source, game);
|
||||
|
|
|
|||
51
Mage/src/main/java/mage/game/events/ExploreEvent.java
Normal file
51
Mage/src/main/java/mage/game/events/ExploreEvent.java
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package mage.game.events;
|
||||
|
||||
import mage.abilities.Ability;
|
||||
import mage.game.permanent.Permanent;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
|
||||
/**
|
||||
* @author Grath
|
||||
*/
|
||||
public class ExploreEvent extends GameEvent {
|
||||
|
||||
private Queue<EventType> eventQueue = new ArrayDeque<>();
|
||||
|
||||
public ExploreEvent(Permanent permanent, Ability source, int amount) {
|
||||
super(EventType.EXPLORE, permanent.getId(), source, permanent.getControllerId(), amount, false);
|
||||
// Populate the queue with a number of EXPLOREs equal to the initial number of EXPLOREs.
|
||||
for (int i = 0; i < amount; ++i) {
|
||||
eventQueue.add(EventType.EXPLORE);
|
||||
}
|
||||
}
|
||||
|
||||
public void doubleExplores() {
|
||||
// Process through the Queue, when we find an EXPLORE add another EXPLORE.
|
||||
Queue<EventType> newQueue = new ArrayDeque<>();
|
||||
for (EventType eventType : eventQueue) {
|
||||
if (eventType == EventType.EXPLORE) {
|
||||
newQueue.add(EventType.EXPLORE);
|
||||
}
|
||||
newQueue.add(eventType);
|
||||
}
|
||||
eventQueue = newQueue;
|
||||
}
|
||||
|
||||
public void addScry() {
|
||||
// Process through the Queue, when we find an EXPLORE add a SCRY before it.
|
||||
Queue<EventType> newQueue = new ArrayDeque<>();
|
||||
for (EventType eventType : eventQueue) {
|
||||
if (eventType == EventType.EXPLORE) {
|
||||
newQueue.add(EventType.SCRY);
|
||||
}
|
||||
newQueue.add(eventType);
|
||||
}
|
||||
eventQueue = newQueue;
|
||||
}
|
||||
|
||||
public Queue<EventType> getEventQueue() {
|
||||
return eventQueue;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue