diff --git a/Mage.Sets/src/mage/cards/g/GilraenDunedainProtector.java b/Mage.Sets/src/mage/cards/g/GilraenDunedainProtector.java index d152620e6c2..61ff9e84c6b 100644 --- a/Mage.Sets/src/mage/cards/g/GilraenDunedainProtector.java +++ b/Mage.Sets/src/mage/cards/g/GilraenDunedainProtector.java @@ -9,7 +9,7 @@ import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbil import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; +import mage.abilities.effects.common.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; diff --git a/Mage.Sets/src/mage/cards/l/LongRoadHome.java b/Mage.Sets/src/mage/cards/l/LongRoadHome.java index 2a8459163b1..a21d22b8cf4 100644 --- a/Mage.Sets/src/mage/cards/l/LongRoadHome.java +++ b/Mage.Sets/src/mage/cards/l/LongRoadHome.java @@ -5,7 +5,7 @@ import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; +import mage.abilities.effects.common.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; diff --git a/Mage.Sets/src/mage/cards/o/OtherworldlyJourney.java b/Mage.Sets/src/mage/cards/o/OtherworldlyJourney.java index 34438712c98..25f70fa43a1 100644 --- a/Mage.Sets/src/mage/cards/o/OtherworldlyJourney.java +++ b/Mage.Sets/src/mage/cards/o/OtherworldlyJourney.java @@ -5,7 +5,7 @@ import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; +import mage.abilities.effects.common.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; diff --git a/Mage.Sets/src/mage/cards/s/SalvationSwan.java b/Mage.Sets/src/mage/cards/s/SalvationSwan.java new file mode 100644 index 00000000000..76a0b1e7f9c --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SalvationSwan.java @@ -0,0 +1,120 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldThisOrAnotherTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect; +import mage.abilities.keyword.FlashAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.ExileZone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.targetpointer.FixedTargets; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class SalvationSwan extends CardImpl { + + private static final FilterControlledPermanent filterBird = new FilterControlledPermanent(SubType.BIRD, "Bird you control"); + private static final FilterControlledCreaturePermanent filterWithoutFlying = new FilterControlledCreaturePermanent("creature you control without flying"); + + static { + filterWithoutFlying.add(Predicates.not(new AbilityPredicate(FlyingAbility.class))); + } + + public SalvationSwan(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.subtype.add(SubType.BIRD); + this.subtype.add(SubType.CLERIC); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flash + this.addAbility(FlashAbility.getInstance()); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever Salvation Swan or another Bird you control enters, exile up to one target creature you control without flying. Return it to the battlefield under its owner's control with a flying counter on it at the beginning of the next end step. + Ability ability = new EntersBattlefieldThisOrAnotherTriggeredAbility( + new SalvationSwanTargetEffect(), filterBird, false, true + ); + ability.addTarget(new TargetControlledCreaturePermanent(0, 1, filterWithoutFlying, false)); + this.addAbility(ability); + } + + private SalvationSwan(final SalvationSwan card) { + super(card); + } + + @Override + public SalvationSwan copy() { + return new SalvationSwan(this); + } +} + +// Similar to Otherwordly Journey +class SalvationSwanTargetEffect extends OneShotEffect { + + SalvationSwanTargetEffect() { + super(Outcome.Benefit); + staticText = " exile up to one target creature you control without flying. " + + "Return it to the battlefield under its owner's control " + + "with a flying counter on it at the beginning of the next end step"; + } + + private SalvationSwanTargetEffect(final SalvationSwanTargetEffect effect) { + super(effect); + } + + @Override + public SalvationSwanTargetEffect copy() { + return new SalvationSwanTargetEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + // We want a unique exile zone for each time this effect resolves. + UUID exileId = UUID.randomUUID(); + String exileName = CardUtil.getSourceIdName(game, source); + permanent.moveToExile(exileId, exileName, source, game); + + OneShotEffect effect = new ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect( + CounterType.FLYING.createInstance(), true + ); + + ExileZone exile = game.getExile().getExileZone(exileId); + if (exile != null) { + exile.setCleanupOnEndTurn(true); + effect.setTargetPointer(new FixedTargets(exile.getCards(game), game)); + } + + // create delayed triggered ability, of note the trigger is created even if no card would be returned. + // TODO: There is currently no way to know which cards will be returned by the trigger. + // Maybe we need a hint to refer to the content of an exile zone? + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect), source); + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Bloomburrow.java b/Mage.Sets/src/mage/sets/Bloomburrow.java index fb7b8aa7bf7..20df742231e 100644 --- a/Mage.Sets/src/mage/sets/Bloomburrow.java +++ b/Mage.Sets/src/mage/sets/Bloomburrow.java @@ -35,6 +35,7 @@ public final class Bloomburrow extends ExpansionSet { cards.add(new SetCardInfo("Oakhollow Village", 258, Rarity.UNCOMMON, mage.cards.o.OakhollowVillage.class)); cards.add(new SetCardInfo("Pearl of Wisdom", 64, Rarity.COMMON, mage.cards.p.PearlOfWisdom.class)); cards.add(new SetCardInfo("Rockface Village", 259, Rarity.UNCOMMON, mage.cards.r.RockfaceVillage.class)); + cards.add(new SetCardInfo("Salvation Swan", 28, Rarity.RARE, mage.cards.s.SalvationSwan.class)); cards.add(new SetCardInfo("Sunshower Druid", 195, Rarity.COMMON, mage.cards.s.SunshowerDruid.class)); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/SalvationSwanTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/SalvationSwanTest.java new file mode 100644 index 00000000000..bec19d13004 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/blb/SalvationSwanTest.java @@ -0,0 +1,44 @@ +package org.mage.test.cards.single.blb; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class SalvationSwanTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.s.SalvationSwan Salvation Swan} {3}{W} + *
+ * Creature — Bird Cleric + * Flash + * Flying + * Whenever Salvation Swan or another Bird you control enters, exile up to one target creature you control without flying. Return it to the battlefield under its owner’s control with a flying counter on it at the beginning of the next end step. + * 3/3 + */ + private static final String swan = "Salvation Swan"; + + @Test + public void test_Simple() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears"); + addCard(Zone.HAND, playerA, swan); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, swan); + addTarget(playerA, "Grizzly Bears"); + + checkExileCount("Bears in exile", 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Grizzly Bears", 1); + + setStopAt(2, PhaseStep.UPKEEP); + execute(); + + assertPermanentCount(playerA, "Grizzly Bears", 1); + assertCounterCount(playerA, "Grizzly Bears", CounterType.FLYING, 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect.java similarity index 94% rename from Mage/src/main/java/mage/abilities/effects/ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect.java rename to Mage/src/main/java/mage/abilities/effects/common/ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect.java index b91cd0e623f..021dda78def 100644 --- a/Mage/src/main/java/mage/abilities/effects/ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect.java @@ -1,7 +1,8 @@ -package mage.abilities.effects; +package mage.abilities.effects.common; import mage.MageObjectReference; import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; import mage.cards.Card; import mage.cards.MeldCard; import mage.constants.Outcome; @@ -11,6 +12,7 @@ import mage.counters.Counters; import mage.game.Game; import mage.players.Player; +// TODO: refactor into ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect public class ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect extends OneShotEffect { private final MageObjectReference objectToReturn; diff --git a/Mage/src/main/java/mage/abilities/effects/common/ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect.java new file mode 100644 index 00000000000..817629134ce --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect.java @@ -0,0 +1,74 @@ +package mage.abilities.effects.common; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.counters.Counter; +import mage.counters.Counters; +import mage.game.Game; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author Susucr + */ +public class ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect extends ReturnToBattlefieldUnderOwnerControlTargetEffect { + + private final Counters counters; + private final String counterText; + + public ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect(Counter counter, boolean returnFromExileZoneOnly) { + this(counter, false, returnFromExileZoneOnly); + } + + public ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect(Counter counter, boolean additional, boolean returnFromExileZoneOnly) { + super(false, returnFromExileZoneOnly); + this.counters = new Counters(); + this.counters.addCounter(counter); + this.counterText = makeText(counter, additional); + } + + protected ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect(final ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect effect) { + super(effect); + this.counters = effect.counters.copy(); + this.counterText = effect.counterText; + } + + @Override + public ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect copy() { + return new ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID targetId : getTargetPointer().getTargets(game, source)) { + game.setEnterWithCounters(targetId, counters.copy()); + } + return super.apply(game, source); + } + + private static String makeText(Counter counter, boolean additional) { + StringBuilder sb = new StringBuilder(" with "); + if (additional) { + sb.append(CardUtil.numberToText(counter.getCount(), "an")); + sb.append(" additional"); + } else { + sb.append(CardUtil.numberToText(counter.getCount(), "a")); + } + sb.append(' '); + sb.append(counter.getName()); + sb.append(" counter"); + if (counter.getCount() != 1) { + sb.append('s'); + } + return sb.toString(); + } + + @Override + public String getText(Mode mode) { + if (staticText != null && !staticText.isEmpty()) { + return staticText; + } + return super.getText(mode) + counterText + (getTargetPointer().isPlural(mode.getTargets()) ? " on them" : " on it"); + } +} \ No newline at end of file