diff --git a/Mage.Sets/src/mage/cards/a/AchillesDavenport.java b/Mage.Sets/src/mage/cards/a/AchillesDavenport.java new file mode 100644 index 00000000000..93036a18e45 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AchillesDavenport.java @@ -0,0 +1,54 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.keyword.FreerunningAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.common.FilterCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AchillesDavenport extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(SubType.ASSASSIN, "Assassins"); + + public AchillesDavenport(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ASSASSIN); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Freerunning {U}{B} + this.addAbility(new FreerunningAbility("{U}{B}")); + + // Menace + this.addAbility(new MenaceAbility()); + + // Other Assassins you control get +1/+1. + this.addAbility(new SimpleStaticAbility(new BoostControlledEffect( + 1, 1, Duration.WhileOnBattlefield, filter, true + ))); + } + + private AchillesDavenport(final AchillesDavenport card) { + super(card); + } + + @Override + public AchillesDavenport copy() { + return new AchillesDavenport(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/ChainAssassination.java b/Mage.Sets/src/mage/cards/c/ChainAssassination.java new file mode 100644 index 00000000000..c4ad07ec30d --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ChainAssassination.java @@ -0,0 +1,77 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.condition.common.MorbidCondition; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.FreerunningAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ChainAssassination extends CardImpl { + + public ChainAssassination(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{B}{B}"); + + // Freerunning {1}{B} + this.addAbility(new FreerunningAbility("{1}{B}")); + + // Destroy target creature. If another creature died this turn, draw a card. + this.getSpellAbility().addEffect(new ChainAssassinationEffect()); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + } + + private ChainAssassination(final ChainAssassination card) { + super(card); + } + + @Override + public ChainAssassination copy() { + return new ChainAssassination(this); + } +} + +class ChainAssassinationEffect extends OneShotEffect { + + ChainAssassinationEffect() { + super(Outcome.Benefit); + staticText = "destroy target creature. If another creature died this turn, draw a card"; + } + + private ChainAssassinationEffect(final ChainAssassinationEffect effect) { + super(effect); + } + + @Override + public ChainAssassinationEffect copy() { + return new ChainAssassinationEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + boolean flag = MorbidCondition.instance.apply(game, source); + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + permanent.destroy(source, game); + if (!flag) { + return true; + } + Player player = game.getPlayer(source.getControllerId()); + if (player != null) { + player.drawCards(1, source, game); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/e/EagleVision.java b/Mage.Sets/src/mage/cards/e/EagleVision.java new file mode 100644 index 00000000000..b431867c4f1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EagleVision.java @@ -0,0 +1,34 @@ +package mage.cards.e; + +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.FreerunningAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EagleVision extends CardImpl { + + public EagleVision(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{U}"); + + // Freerunning {1}{U} + this.addAbility(new FreerunningAbility("{1}{U}")); + + // Draw three cards. + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(3)); + } + + private EagleVision(final EagleVision card) { + super(card); + } + + @Override + public EagleVision copy() { + return new EagleVision(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RestartSequence.java b/Mage.Sets/src/mage/cards/r/RestartSequence.java new file mode 100644 index 00000000000..b6f22fdcd66 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RestartSequence.java @@ -0,0 +1,37 @@ +package mage.cards.r; + +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.keyword.FreerunningAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RestartSequence extends CardImpl { + + public RestartSequence(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}"); + + // Freerunning {1}{B} + this.addAbility(new FreerunningAbility("{1}{B}")); + + // Return target creature card from your graveyard to the battlefield. + this.getSpellAbility().addEffect(new ReturnFromGraveyardToBattlefieldTargetEffect()); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); + } + + private RestartSequence(final RestartSequence card) { + super(card); + } + + @Override + public RestartSequence copy() { + return new RestartSequence(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AssassinsCreed.java b/Mage.Sets/src/mage/sets/AssassinsCreed.java index 5a995954b3f..8a087f60792 100644 --- a/Mage.Sets/src/mage/sets/AssassinsCreed.java +++ b/Mage.Sets/src/mage/sets/AssassinsCreed.java @@ -22,15 +22,18 @@ public final class AssassinsCreed extends ExpansionSet { this.hasBoosters = false; cards.add(new SetCardInfo("Abstergo Entertainment", 79, Rarity.RARE, mage.cards.a.AbstergoEntertainment.class)); + cards.add(new SetCardInfo("Achilles Davenport", 294, Rarity.RARE, mage.cards.a.AchillesDavenport.class)); cards.add(new SetCardInfo("Assassin's Trophy", 166, Rarity.RARE, mage.cards.a.AssassinsTrophy.class)); cards.add(new SetCardInfo("Aya of Alexandria", 48, Rarity.RARE, mage.cards.a.AyaOfAlexandria.class)); cards.add(new SetCardInfo("Basim Ibn Ishaq", 49, Rarity.RARE, mage.cards.b.BasimIbnIshaq.class)); cards.add(new SetCardInfo("Bayek of Siwa", 50, Rarity.RARE, mage.cards.b.BayekOfSiwa.class)); cards.add(new SetCardInfo("Cathartic Reunion", 94, Rarity.UNCOMMON, mage.cards.c.CatharticReunion.class)); + cards.add(new SetCardInfo("Chain Assassination", 23, Rarity.UNCOMMON, mage.cards.c.ChainAssassination.class)); cards.add(new SetCardInfo("Cleopatra, Exiled Pharaoh", 52, Rarity.MYTHIC, mage.cards.c.CleopatraExiledPharaoh.class)); cards.add(new SetCardInfo("Coastal Piracy", 84, Rarity.UNCOMMON, mage.cards.c.CoastalPiracy.class)); cards.add(new SetCardInfo("Conspiracy", 88, Rarity.RARE, mage.cards.c.Conspiracy.class)); cards.add(new SetCardInfo("Cover of Darkness", 89, Rarity.RARE, mage.cards.c.CoverOfDarkness.class)); + cards.add(new SetCardInfo("Eagle Vision", 17, Rarity.UNCOMMON, mage.cards.e.EagleVision.class)); cards.add(new SetCardInfo("Eivor, Battle-Ready", 274, Rarity.MYTHIC, mage.cards.e.EivorBattleReady.class)); cards.add(new SetCardInfo("Escarpment Fortress", 278, Rarity.RARE, mage.cards.e.EscarpmentFortress.class)); cards.add(new SetCardInfo("Ezio, Blade of Vengeance", 275, Rarity.MYTHIC, mage.cards.e.EzioBladeOfVengeance.class)); @@ -46,6 +49,7 @@ public final class AssassinsCreed extends ExpansionSet { cards.add(new SetCardInfo("Raven Clan War-Axe", 297, Rarity.RARE, mage.cards.r.RavenClanWarAxe.class)); cards.add(new SetCardInfo("Reconstruct History", 97, Rarity.UNCOMMON, mage.cards.r.ReconstructHistory.class)); cards.add(new SetCardInfo("Rest in Peace", 83, Rarity.RARE, mage.cards.r.RestInPeace.class)); + cards.add(new SetCardInfo("Restart Sequence", 30, Rarity.UNCOMMON, mage.cards.r.RestartSequence.class)); cards.add(new SetCardInfo("Royal Assassin", 93, Rarity.RARE, mage.cards.r.RoyalAssassin.class)); cards.add(new SetCardInfo("Silent Clearing", 115, Rarity.RARE, mage.cards.s.SilentClearing.class)); cards.add(new SetCardInfo("Sunbaked Canyon", 111, Rarity.RARE, mage.cards.s.SunbakedCanyon.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/FreerunningTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/FreerunningTest.java new file mode 100644 index 00000000000..6e5558a1ee6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/FreerunningTest.java @@ -0,0 +1,91 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommanderDuelBase; + +/** + * @author TheElk801 + */ +public class FreerunningTest extends CardTestCommanderDuelBase { + + private static final String vision = "Eagle Vision"; + + @Test + public void testRegular() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.HAND, playerA, vision); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 3); + assertTappedCount("Island", true, 5); + } + + private static final String poisoner = "Hired Poisoner"; + + @Test + public void testAssassin() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, poisoner); + addCard(Zone.HAND, playerA, vision); + + attack(1, playerA, poisoner, playerB); + + setChoice(playerA, true); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 3); + } + + private static final String goblin = "Raging Goblin"; + + @Test + public void testCommander() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 2 + 1); + addCard(Zone.COMMAND, playerA, goblin); + addCard(Zone.HAND, playerA, vision); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblin); + + attack(1, playerA, goblin, playerB); + + setChoice(playerA, true); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 3); + } + + @Test + public void testNeither() { + addCard(Zone.BATTLEFIELD, playerA, "Volcanic Island", 5 + 1); + addCard(Zone.HAND, playerA, goblin); + addCard(Zone.HAND, playerA, vision); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, goblin); + + attack(1, playerA, goblin, playerB); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, vision); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 3); + assertTappedCount("Volcanic Island", true, 5 + 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/FreerunningAbility.java b/Mage/src/main/java/mage/abilities/keyword/FreerunningAbility.java new file mode 100644 index 00000000000..93d653edca6 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/FreerunningAbility.java @@ -0,0 +1,105 @@ +package mage.abilities.keyword; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.costs.AlternativeSourceCostsImpl; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.constants.SubType; +import mage.constants.WatcherScope; +import mage.filter.predicate.mageobject.CommanderPredicate; +import mage.game.Game; +import mage.game.events.DamagedEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.watchers.Watcher; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class FreerunningAbility extends AlternativeSourceCostsImpl { + + private static final String FREERUNNING_KEYWORD = "Freerunning"; + private static final String FREERUNNING_REMINDER = "You may cast this spell for its freerunning cost " + + "if you dealt combat damage to a player this turn with an Assassin or commander"; + + public FreerunningAbility(String manaString) { + super(FREERUNNING_KEYWORD, FREERUNNING_REMINDER, manaString); + this.setRuleAtTheTop(true); + this.addWatcher(new FreerunningWatcher()); + this.addHint(FreerunningCondition.getHint()); + } + + private FreerunningAbility(final FreerunningAbility ability) { + super(ability); + } + + @Override + public FreerunningAbility copy() { + return new FreerunningAbility(this); + } + + @Override + public boolean isAvailable(Ability source, Game game) { + return FreerunningCondition.instance.apply(game, source); + } + + public static String getActivationKey() { + return getActivationKey(FREERUNNING_KEYWORD); + } +} + +enum FreerunningCondition implements Condition { + instance; + private static final Hint hint = new ConditionHint(instance, "Freerunning can be used"); + + public static Hint getHint() { + return hint; + } + + @Override + public boolean apply(Game game, Ability source) { + return FreerunningWatcher.checkPlayer(source.getControllerId(), game); + } +} + +class FreerunningWatcher extends Watcher { + + private final Set players = new HashSet<>(); + + FreerunningWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.DAMAGED_PLAYER + || !((DamagedEvent) event).isCombatDamage()) { + return; + } + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId()); + if (permanent != null + && (permanent.hasSubtype(SubType.ASSASSIN, game) + || CommanderPredicate.instance.apply(permanent, game))) { + players.add(permanent.getControllerId()); + } + } + + @Override + public void reset() { + players.clear(); + super.reset(); + } + + static boolean checkPlayer(UUID playerId, Game game) { + return game + .getState() + .getWatcher(FreerunningWatcher.class) + .players + .contains(playerId); + } +} diff --git a/Utils/keywords.txt b/Utils/keywords.txt index 0390de0afd3..063309e8228 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -59,6 +59,7 @@ Forestcycling|cost| Forestwalk|new| Foretell|card, manaString| For Mirrodin!|new| +Freerunning|manaString| Friends forever|instance| Haste|instance| Hexproof|instance|