implement [BLB] Salvation Swan

This commit is contained in:
Susucre 2024-06-29 18:38:00 +02:00
parent d29c57d2af
commit 62a99a0497
8 changed files with 245 additions and 4 deletions

View file

@ -9,7 +9,7 @@ import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbil
import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; import mage.abilities.effects.common.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;

View file

@ -5,7 +5,7 @@ import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; import mage.abilities.effects.common.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;

View file

@ -5,7 +5,7 @@ import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect; import mage.abilities.effects.common.ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;

View file

@ -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;
}
}

View file

@ -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("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("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("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)); cards.add(new SetCardInfo("Sunshower Druid", 195, Rarity.COMMON, mage.cards.s.SunshowerDruid.class));
} }
} }

View file

@ -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}
* <p>
* 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 owners 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);
}
}

View file

@ -1,7 +1,8 @@
package mage.abilities.effects; package mage.abilities.effects.common;
import mage.MageObjectReference; import mage.MageObjectReference;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card; import mage.cards.Card;
import mage.cards.MeldCard; import mage.cards.MeldCard;
import mage.constants.Outcome; import mage.constants.Outcome;
@ -11,6 +12,7 @@ import mage.counters.Counters;
import mage.game.Game; import mage.game.Game;
import mage.players.Player; import mage.players.Player;
// TODO: refactor into ReturnToBattlefieldUnderOwnerControlWithCounterTargetEffect
public class ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect extends OneShotEffect { public class ReturnMORToBattlefieldUnderOwnerControlWithCounterEffect extends OneShotEffect {
private final MageObjectReference objectToReturn; private final MageObjectReference objectToReturn;

View file

@ -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");
}
}