diff --git a/Mage.Sets/src/mage/cards/k/KavLandseeker.java b/Mage.Sets/src/mage/cards/k/KavLandseeker.java new file mode 100644 index 00000000000..7b9eef99637 --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KavLandseeker.java @@ -0,0 +1,82 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.SacrificeTargetEffect; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.token.LanderToken; +import mage.game.permanent.token.Token; +import mage.target.targetpointer.FixedTargets; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class KavLandseeker extends CardImpl { + + public KavLandseeker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + + this.subtype.add(SubType.KAVU); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Menace + this.addAbility(new MenaceAbility()); + + // When this creature enters, create a Lander token. At the beginning of the end step on your next turn, sacrifice that token. + this.addAbility(new EntersBattlefieldTriggeredAbility(new KavLandseekerEffect())); + } + + private KavLandseeker(final KavLandseeker card) { + super(card); + } + + @Override + public KavLandseeker copy() { + return new KavLandseeker(this); + } +} + +class KavLandseekerEffect extends OneShotEffect { + + KavLandseekerEffect() { + super(Outcome.Benefit); + staticText = "create a Lander token. " + + "At the beginning of the end step on your next turn, sacrifice that token"; + } + + private KavLandseekerEffect(final KavLandseekerEffect effect) { + super(effect); + } + + @Override + public KavLandseekerEffect copy() { + return new KavLandseekerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Token token = new LanderToken(); + token.putOntoBattlefield(1, game, source, source.getControllerId()); + game.addDelayedTriggeredAbility(new AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility( + new SacrificeTargetEffect() + .setTargetPointer(new FixedTargets(token, game)) + .setText("sacrifice that token"), + GameEvent.EventType.END_TURN_STEP_PRE + ), source); + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/MeanderingTowershell.java b/Mage.Sets/src/mage/cards/m/MeanderingTowershell.java index b9074aad0b8..f4b4e6efc4b 100644 --- a/Mage.Sets/src/mage/cards/m/MeanderingTowershell.java +++ b/Mage.Sets/src/mage/cards/m/MeanderingTowershell.java @@ -1,39 +1,40 @@ package mage.cards.m; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.DelayedTriggeredAbility; import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.keyword.IslandwalkAbility; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Outcome; +import mage.constants.SubType; import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.UUID; + /** * As Meandering Towershell returns to the battlefield because of the delayed * triggered ability, you choose which opponent or opposing planeswalker it's * attacking. It doesn't have to attack the same opponent or opposing * planeswalker that it was when it was exiled. - * + * -- * If Meandering Towershell enters the battlefield attacking, it wasn't declared * as an attacking creature that turn. Abilities that trigger when a creature * attacks, including its own triggered ability, won't trigger. - * + * -- * On the turn Meandering Towershell attacks and is exiled, raid abilities will * see it as a creature that attacked. Conversely, on the turn Meandering * Towershell enters the battlefield attacking, raid abilities will not. - * + * -- * If you attack with a Meandering Towershell that you don't own, you'll control * it when it returns to the battlefield. * @@ -42,7 +43,7 @@ import mage.players.Player; public final class MeanderingTowershell extends CardImpl { public MeanderingTowershell(UUID ownerId, CardSetInfo setInfo) { - super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{3}{G}{G}"); + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}{G}"); this.subtype.add(SubType.TURTLE); this.power = new MageInt(5); @@ -90,58 +91,13 @@ class MeanderingTowershellEffect extends OneShotEffect { Permanent sourcePermanent = game.getPermanent(source.getSourceId()); if (controller != null && sourcePermanent != null) { controller.moveCardToExileWithInfo(sourcePermanent, null, "", source, game, Zone.BATTLEFIELD, true); - game.addDelayedTriggeredAbility(new AtBeginningNextDeclareAttackersStepNextTurnDelayedTriggeredAbility(), source); + game.addDelayedTriggeredAbility(new AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility(new MeanderingTowershellReturnEffect(), GameEvent.EventType.DECLARE_ATTACKERS_STEP_PRE), source); return true; } return false; } } -class AtBeginningNextDeclareAttackersStepNextTurnDelayedTriggeredAbility extends DelayedTriggeredAbility { - - private int startingTurn; - - public AtBeginningNextDeclareAttackersStepNextTurnDelayedTriggeredAbility() { - super(new MeanderingTowershellReturnEffect()); - } - - private AtBeginningNextDeclareAttackersStepNextTurnDelayedTriggeredAbility(final AtBeginningNextDeclareAttackersStepNextTurnDelayedTriggeredAbility ability) { - super(ability); - this.startingTurn = ability.startingTurn; - } - - @Override - public void init(Game game) { - startingTurn = game.getTurnNum(); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.DECLARED_ATTACKERS; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (event.getPlayerId().equals(this.controllerId)) { - if (game.getTurnNum() != startingTurn) { - return true; - } - } - return false; - } - - @Override - public String getRule() { - return "Return it to the battlefield under your control tapped and attacking at the beginning of the next declare attackers step on your next turn."; - } - - @Override - public AtBeginningNextDeclareAttackersStepNextTurnDelayedTriggeredAbility copy() { - return new AtBeginningNextDeclareAttackersStepNextTurnDelayedTriggeredAbility(this); - } - -} - class MeanderingTowershellReturnEffect extends OneShotEffect { MeanderingTowershellReturnEffect() { diff --git a/Mage.Sets/src/mage/sets/EdgeOfEternities.java b/Mage.Sets/src/mage/sets/EdgeOfEternities.java index d959fc1905f..ab8ff836f08 100644 --- a/Mage.Sets/src/mage/sets/EdgeOfEternities.java +++ b/Mage.Sets/src/mage/sets/EdgeOfEternities.java @@ -114,6 +114,7 @@ public final class EdgeOfEternities extends ExpansionSet { cards.add(new SetCardInfo("Island", 269, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Island", 270, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Island", 368, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("Kav Landseeker", 138, Rarity.COMMON, mage.cards.k.KavLandseeker.class)); cards.add(new SetCardInfo("Kavaron Harrier", 139, Rarity.UNCOMMON, mage.cards.k.KavaronHarrier.class)); cards.add(new SetCardInfo("Kavaron, Memorial World", 255, Rarity.MYTHIC, mage.cards.k.KavaronMemorialWorld.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Kavaron, Memorial World", 281, Rarity.MYTHIC, mage.cards.k.KavaronMemorialWorld.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/eoe/KavLandseekerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/eoe/KavLandseekerTest.java new file mode 100644 index 00000000000..779f7e16ab3 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/eoe/KavLandseekerTest.java @@ -0,0 +1,56 @@ +package org.mage.test.cards.single.eoe; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class KavLandseekerTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.k.KavLandseeker Kav Landseeker} {3}{R} + * Creature — Kavu Soldier + * Menace (This creature can’t be blocked except by two or more creatures.) + * When this creature enters, create a Lander token. At the beginning of the end step on your next turn, sacrifice that token. (It’s an artifact with “{2}, {T}, Sacrifice this token: Search your library for a basic land card, put it onto the battlefield tapped, then shuffle.”) + * 4/3 + */ + private static final String kav = "Kav Landseeker"; + + @Test + public void test_Simple() { + addCard(Zone.HAND, playerA, kav); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kav); + + checkPermanentCount("T3: playerA has a Lander Token", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, playerA, "Lander Token", 1); + + setStopAt(4, PhaseStep.PRECOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertPermanentCount(playerA, "Lander Token", 0); + } + + @Test + public void test_TimeStop() { + addCard(Zone.HAND, playerA, kav); + addCard(Zone.HAND, playerA, "Time Stop"); // end the turn + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 6); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, kav); + + checkPermanentCount("T3: playerA has a Lander Token", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, playerA, "Lander Token", 1); + castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "Time Stop"); + + setStopAt(6, PhaseStep.PRECOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + // the delayed trigger has been cleaned up since the "next turn" had no end step. + assertPermanentCount(playerA, "Lander Token", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ktk/MeanderingTowershellTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ktk/MeanderingTowershellTest.java new file mode 100644 index 00000000000..ed1a651293d --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ktk/MeanderingTowershellTest.java @@ -0,0 +1,64 @@ +package org.mage.test.cards.single.ktk; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class MeanderingTowershellTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.m.MeanderingTowershell Meandering Towershell} {3}{G}{G} + * Creature — Turtle + * Islandwalk (This creature can’t be blocked as long as defending player controls an Island.) + * Whenever this creature attacks, exile it. Return it to the battlefield under your control tapped and attacking at the beginning of the declare attackers step on your next turn. + * 5/9 + */ + private static final String towershell = "Meandering Towershell"; + + @Test + public void test_Simple() { + addCard(Zone.BATTLEFIELD, playerA, towershell); + + attack(1, playerA, towershell, playerB); + + checkPermanentCount("T3 First Main: no Towershell", 3, PhaseStep.PRECOMBAT_MAIN, playerA, playerA, "Meandering Powershell", 0); + checkLife("T3 First Main: playerB at 20 life", 3, PhaseStep.PRECOMBAT_MAIN, playerB, 20); + + // Meandering Towershell comes back attacking. + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, towershell, 1); + assertLife(playerB, 20 - 5); + } + + @Test + public void test_TimeStop() { + addCard(Zone.BATTLEFIELD, playerA, towershell); + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + addCard(Zone.HAND, playerA, "Time Stop"); + + attack(1, playerA, towershell, playerB); + + checkPermanentCount("T3 First Main: no Towershell", 3, PhaseStep.PRECOMBAT_MAIN, playerA, playerA, "Meandering Powershell", 0); + checkLife("T3 First Main: playerB at 20 life", 3, PhaseStep.PRECOMBAT_MAIN, playerB, 20); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Time Stop"); + + // Meandering Towershell never comes back on future turns. + + setStrictChooseMode(true); + setStopAt(6, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertExileCount(playerA, towershell, 1); + assertPermanentCount(playerA, towershell, 0); + assertLife(playerB, 20); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility.java new file mode 100644 index 00000000000..f6ceb05b7c0 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/delayed/AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility.java @@ -0,0 +1,83 @@ +package mage.abilities.common.delayed; + +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.constants.Duration; +import mage.game.Game; +import mage.game.events.GameEvent; + +/** + * @author Susucr + */ +public class AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility extends DelayedTriggeredAbility { + + private GameEvent.EventType stepEvent; + private int nextTurn = -1; // once the controller starts a new turn, register it to trigger that turn. + private boolean isActive = true; + + public AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility(Effect effect, GameEvent.EventType stepEvent) { + this(effect, stepEvent, false); + } + + public AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility(Effect effect, GameEvent.EventType stepEvent, boolean optional) { + super(effect, Duration.Custom, true, optional); + this.stepEvent = stepEvent; + this.setTriggerPhrase(generateTriggerPhrase()); + } + + private AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility(final AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility ability) { + super(ability); + this.nextTurn = ability.nextTurn; + this.isActive = ability.isActive; + this.stepEvent = ability.stepEvent; + } + + @Override + public AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility copy() { + return new AtTheBeginOfStepOfYourNextTurnDelayedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == stepEvent + || event.getType() == GameEvent.EventType.BEGIN_TURN; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!isControlledBy(event.getPlayerId())) { + // not your turn. + return false; + } + + int turn = game.getTurnNum(); + switch (event.getType()) { + case BEGIN_TURN: + // We register the turn number at the start of your next turn. + // This is in order to not trigger if that turn ends without an end step. + if (this.nextTurn == -1) { + this.nextTurn = turn; + } else if (turn > this.nextTurn) { + this.isActive = false; // to have the delayed trigger being cleaned up + } + return false; + default: + return turn == this.nextTurn && event.getType() == stepEvent; + } + } + + @Override + public boolean isInactive(Game game) { + return super.isInactive(game) || !isActive; + } + + private String generateTriggerPhrase() { + switch (stepEvent) { + case END_TURN_STEP_PRE: + return "At the beginning of the end step on your next turn, "; + case DECLARE_ATTACKERS_STEP_PRE: + return "At the beginning of the declare attackers step on your next turn, "; + } + throw new IllegalArgumentException("stepEvent only supports steps events"); + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 9a12cc41e44..98130b20d32 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -40,6 +40,7 @@ public class GameEvent implements Serializable { PREVENT_DAMAGE, PREVENTED_DAMAGE, //Turn-based events PLAY_TURN, EXTRA_TURN, + BEGIN_TURN, // event fired on actual begin of turn. CHANGE_PHASE, PHASE_CHANGED, CHANGE_STEP, STEP_CHANGED, BEGINNING_PHASE, BEGINNING_PHASE_PRE, BEGINNING_PHASE_POST, // The normal beginning phase -- at the beginning of turn diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 94d126e11f5..c743ff1e0f9 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -547,6 +547,8 @@ public abstract class PlayerImpl implements Player, Serializable { resetLandsPlayed(); updateRange(game); game.getState().removeTurnStartEffect(game); + GameEvent event = new GameEvent(GameEvent.EventType.BEGIN_TURN, null, null, getId()); + game.fireEvent(event); } @Override