diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 68881ef7f60..a2df59f5059 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -4,15 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class Aetherdrift extends ExpansionSet { - private static final List unfinished = Arrays.asList("Aether Syphon", "Amonkhet Raceway", "Avishkar Raceway", "Burnout Bashtronaut", "Embalmed Ascendant", "Endrider Catalyzer", "Endrider Spikespitter", "Far Fortune, End Boss", "Gas Guzzler", "Gastal Raider", "Gastal Thrillseeker", "Glitch Ghost Surveyor", "Goblin Surveyor", "Hazoret, Godseeker", "Hour of Victory", "Howlsquad Heavy", "Kickoff Celebrations", "Leonin Surveyor", "Lightwheel Enhancements", "Loxodon Surveyor", "Mendicant Core, Guidelight", "Momentum Breaker", "Muraganda Raceway", "Mutant Surveyor", "Nesting Bot", "Outpace Oblivion", "Perilous Snare", "Point the Way", "Pride of the Road", "Racers' Scoreboard", "Risen Necroregent", "Samut, the Driving Force", "Slick Imitator", "Starting Column", "Streaking Oilgorger", "Swiftwing Assailant", "The Speed Demon", "Vnwxt, Verbose Host", "Walking Sarcophagus", "Zahur, Glory's Past"); private static final Aetherdrift instance = new Aetherdrift(); public static Aetherdrift getInstance() { @@ -211,7 +207,5 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Wreck Remover", 247, Rarity.COMMON, mage.cards.w.WreckRemover.class)); cards.add(new SetCardInfo("Wreckage Wickerfolk", 110, Rarity.COMMON, mage.cards.w.WreckageWickerfolk.class)); cards.add(new SetCardInfo("Wretched Doll", 111, Rarity.UNCOMMON, mage.cards.w.WretchedDoll.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java new file mode 100644 index 00000000000..2c4f6caf823 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/designations/StartYourEnginesTest.java @@ -0,0 +1,202 @@ +package org.mage.test.cards.designations; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.players.Player; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class StartYourEnginesTest extends CardTestPlayerBase { + + private static final String sarcophagus = "Walking Sarcophagus"; + + private void assertSpeed(Player player, int speed) { + Assert.assertEquals(player.getName() + " speed should be " + speed, speed, player.getSpeed()); + } + + @Test + public void testRegular() { + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 0); + assertSpeed(playerB, 0); + } + + @Test + public void testSpeed1() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 1); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2, 1); + } + + private static final String goblet = "Onyx Goblet"; + + @Test + public void testSpeed2() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 2); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2, 1); + } + + @Test + public void testSpeed1OppTurn() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 1); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2, 1); + } + + @Test + public void testSpeed3() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 3); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2, 1); + } + + @Test + public void testSpeed4() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 4); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2 + 1, 1 + 2); + } + + @Test + public void testSpeed5() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerA, goblet); + addCard(Zone.HAND, playerA, sarcophagus); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(3, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(5, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + activateAbility(7, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Target", playerB); + + setStrictChooseMode(true); + setStopAt(7, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 4); + assertSpeed(playerB, 0); + assertPowerToughness(playerA, sarcophagus, 2 + 1, 1 + 2); + } + + private static final String surveyor = "Loxodon Surveyor"; + + @Test + public void testSpeed4Graveyard() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 3); + addCard(Zone.GRAVEYARD, playerA, surveyor); + + runCode("Increase player speed", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + player.initSpeed(game); + player.increaseSpeed(game); + player.increaseSpeed(game); + player.increaseSpeed(game); + }); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{3}"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 4); + assertSpeed(playerB, 0); + assertGraveyardCount(playerA, surveyor, 0); + assertExileCount(playerA, surveyor, 1); + } + + private static final String mindControl = "Mind Control"; + + @Test + public void testSpeedChangeControl() { + addCard(Zone.BATTLEFIELD, playerA, "Wastes", 2); + addCard(Zone.BATTLEFIELD, playerB, "Island", 5); + addCard(Zone.HAND, playerA, sarcophagus); + addCard(Zone.HAND, playerB, mindControl); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, sarcophagus); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, mindControl, sarcophagus); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertSpeed(playerA, 1); + assertSpeed(playerB, 1); + assertPowerToughness(playerB, sarcophagus, 2, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index cef50ca4a8e..8d9394141ef 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -3977,6 +3977,21 @@ public class TestPlayer implements Player { return computerPlayer.isDrawsOnOpponentsTurn(); } + @Override + public int getSpeed() { + return computerPlayer.getSpeed(); + } + + @Override + public void initSpeed(Game game) { + computerPlayer.initSpeed(game); + } + + @Override + public void increaseSpeed(Game game) { + computerPlayer.increaseSpeed(game); + } + @Override public void setPayManaMode(boolean payManaMode) { computerPlayer.setPayManaMode(payManaMode); diff --git a/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java b/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java index 4a66080a057..ce34f866ee1 100644 --- a/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java +++ b/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java @@ -38,9 +38,21 @@ class MaxSpeedAbilityEffect extends ContinuousEffectImpl { private final Ability ability; + private static Duration getDuration(Ability ability) { + switch (ability.getZone()) { + case BATTLEFIELD: + return Duration.WhileOnBattlefield; + case GRAVEYARD: + return Duration.WhileInGraveyard; + default: + return Duration.Custom; + } + } + MaxSpeedAbilityEffect(Ability ability) { - super(Duration.Custom, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + super(getDuration(ability), Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); this.ability = ability; + this.ability.setRuleVisible(false); } private MaxSpeedAbilityEffect(final MaxSpeedAbilityEffect effect) { diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java index b06aa28ecfe..12ca32d02a9 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/ControllerSpeedCount.java @@ -4,6 +4,9 @@ import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.game.Game; +import mage.players.Player; + +import java.util.Optional; /** * @author TheElk801 @@ -13,8 +16,10 @@ public enum ControllerSpeedCount implements DynamicValue { @Override public int calculate(Game game, Ability sourceAbility, Effect effect) { - // TODO: Implement this - return 0; + return Optional + .ofNullable(game.getPlayer(sourceAbility.getControllerId())) + .map(Player::getSpeed) + .orElse(0); } @Override diff --git a/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java b/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java index 1c8d8c5516c..ecbafd688f4 100644 --- a/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/StartYourEnginesAbility.java @@ -1,17 +1,21 @@ package mage.abilities.keyword; import mage.abilities.StaticAbility; +import mage.abilities.dynamicvalue.common.ControllerSpeedCount; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; import mage.constants.Zone; /** - * TODO: Implement this - * * @author TheElk801 */ public class StartYourEnginesAbility extends StaticAbility { + private static final Hint hint = new ValueHint("Your current speed", ControllerSpeedCount.instance); + public StartYourEnginesAbility() { super(Zone.BATTLEFIELD, null); + this.addHint(hint); } private StartYourEnginesAbility(final StartYourEnginesAbility ability) { @@ -25,6 +29,6 @@ public class StartYourEnginesAbility extends StaticAbility { @Override public String getRule() { - return "Start your engines!"; + return "start your engines!"; } } diff --git a/Mage/src/main/java/mage/designations/DesignationType.java b/Mage/src/main/java/mage/designations/DesignationType.java index 62b77b28c61..dd95ff9808f 100644 --- a/Mage/src/main/java/mage/designations/DesignationType.java +++ b/Mage/src/main/java/mage/designations/DesignationType.java @@ -6,8 +6,8 @@ package mage.designations; public enum DesignationType { THE_MONARCH("The Monarch"), // global CITYS_BLESSING("City's Blessing"), // per player - THE_INITIATIVE("The Initiative"); // global - + THE_INITIATIVE("The Initiative"), // global + SPEED("Speed"); // per player private final String text; DesignationType(String text) { @@ -18,5 +18,4 @@ public enum DesignationType { public String toString() { return text; } - } diff --git a/Mage/src/main/java/mage/designations/Speed.java b/Mage/src/main/java/mage/designations/Speed.java new file mode 100644 index 00000000000..b3529f50fb2 --- /dev/null +++ b/Mage/src/main/java/mage/designations/Speed.java @@ -0,0 +1,108 @@ +package mage.designations; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.OneShotEffect; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.Player; + +import java.util.Optional; + +/** + * @author TheElk801 + */ +public class Speed extends Designation { + + public Speed() { + super(DesignationType.SPEED); + addAbility(new SpeedTriggeredAbility()); + } + + private Speed(final Speed card) { + super(card); + } + + @Override + public Speed copy() { + return new Speed(this); + } +} + +class SpeedTriggeredAbility extends TriggeredAbilityImpl { + + SpeedTriggeredAbility() { + super(Zone.ALL, new SpeedEffect()); + setTriggersLimitEachTurn(1); + } + + private SpeedTriggeredAbility(final SpeedTriggeredAbility ability) { + super(ability); + } + + @Override + public SpeedTriggeredAbility copy() { + return new SpeedTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.LOST_LIFE_BATCH_FOR_ONE_PLAYER; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return game.isActivePlayer(getControllerId()) + && game + .getOpponents(getControllerId()) + .contains(event.getTargetId()); + } + + @Override + public boolean checkInterveningIfClause(Game game) { + return Optional + .ofNullable(getControllerId()) + .map(game::getPlayer) + .map(Player::getSpeed) + .map(x -> x < 4) + .orElse(false); + } + + @Override + public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) { + return true; + } + + @Override + public String getRule() { + return "Whenever one or more opponents lose life during your turn, if your speed is less than 4, " + + "increase your speed by 1. This ability triggers only once each turn."; + } +} + +class SpeedEffect extends OneShotEffect { + + SpeedEffect() { + super(Outcome.Benefit); + } + + private SpeedEffect(final SpeedEffect effect) { + super(effect); + } + + @Override + public SpeedEffect copy() { + return new SpeedEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Optional.ofNullable(source.getControllerId()) + .map(game::getPlayer) + .ifPresent(player -> player.increaseSpeed(game)); + return true; + } +} diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 1f554dbdd4e..fc68ddad89d 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -2862,6 +2862,11 @@ public abstract class GameImpl implements Game { } } } + if (perm.getAbilities(this).containsClass(StartYourEnginesAbility.class)) { + Optional.ofNullable(perm.getControllerId()) + .map(this::getPlayer) + .ifPresent(player -> player.initSpeed(this)); + } } //201300713 - 704.5k // If a player controls two or more legendary permanents with the same name, that player diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 9566348bc7c..cdf5a8bed3c 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -216,6 +216,12 @@ public interface Player extends MageItem, Copyable { boolean isDrawsOnOpponentsTurn(); + int getSpeed(); + + void initSpeed(Game game); + + void increaseSpeed(Game game); + /** * Returns alternative casting costs a player can cast spells for * diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index b1b3434cf55..0966ac467e2 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -27,6 +27,7 @@ import mage.counters.CounterType; import mage.counters.Counters; import mage.designations.Designation; import mage.designations.DesignationType; +import mage.designations.Speed; import mage.filter.FilterCard; import mage.filter.FilterMana; import mage.filter.FilterPermanent; @@ -153,6 +154,7 @@ public abstract class PlayerImpl implements Player, Serializable { protected boolean canPlotFromTopOfLibrary = false; protected boolean drawsFromBottom = false; protected boolean drawsOnOpponentsTurn = false; + protected int speed = 0; protected FilterPermanent sacrificeCostFilter; protected List alternativeSourceCosts = new ArrayList<>(); @@ -252,6 +254,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.canPlotFromTopOfLibrary = player.canPlotFromTopOfLibrary; this.drawsFromBottom = player.drawsFromBottom; this.drawsOnOpponentsTurn = player.drawsOnOpponentsTurn; + this.speed = player.speed; this.attachments.addAll(player.attachments); @@ -367,6 +370,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.drawsFromBottom = player.isDrawsFromBottom(); this.drawsOnOpponentsTurn = player.isDrawsOnOpponentsTurn(); this.alternativeSourceCosts = CardUtil.deepCopyObject(((PlayerImpl) player).alternativeSourceCosts); + this.speed = player.getSpeed(); this.topCardRevealed = player.isTopCardRevealed(); @@ -480,6 +484,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.canPlotFromTopOfLibrary = false; this.drawsFromBottom = false; this.drawsOnOpponentsTurn = false; + this.speed = 0; this.sacrificeCostFilter = null; this.alternativeSourceCosts.clear(); @@ -4674,6 +4679,29 @@ public abstract class PlayerImpl implements Player, Serializable { return drawsOnOpponentsTurn; } + @Override + public int getSpeed() { + return speed; + } + + @Override + public void initSpeed(Game game) { + if (speed > 0) { + return; + } + speed = 1; + game.getState().addDesignation(new Speed(), game, getId()); + game.informPlayers(this.getLogName() + "'s speed is now 1."); + } + + @Override + public void increaseSpeed(Game game) { + if (speed < 4) { + speed++; + game.informPlayers(this.getLogName() + "'s speed has increased to " + speed); + } + } + @Override public boolean autoLoseGame() { return false;