diff --git a/Mage.Sets/src/mage/cards/a/ALittleChat.java b/Mage.Sets/src/mage/cards/a/ALittleChat.java index ff34451038..fd65e1b953 100644 --- a/Mage.Sets/src/mage/cards/a/ALittleChat.java +++ b/Mage.Sets/src/mage/cards/a/ALittleChat.java @@ -18,7 +18,7 @@ public final class ALittleChat extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{U}"); // Casualty 1 - this.addAbility(new CasualtyAbility(this, 1)); + this.addAbility(new CasualtyAbility(1)); // Look at the top two cards of your library. Put one of them into your hand and the other on the bottom of your library. this.getSpellAbility().addEffect(new LookLibraryAndPickControllerEffect(2, 1, PutCards.HAND, PutCards.BOTTOM_ANY)); diff --git a/Mage.Sets/src/mage/cards/a/AerialExtortionist.java b/Mage.Sets/src/mage/cards/a/AerialExtortionist.java new file mode 100644 index 0000000000..2026bc10d6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AerialExtortionist.java @@ -0,0 +1,93 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SpellCastOpponentTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.meta.OrTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetNonlandPermanent; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class AerialExtortionist extends CardImpl { + public AerialExtortionist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}{W}"); + + this.subtype.add(SubType.BIRD); + this.subtype.add(SubType.SOLDIER); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever Aerial Extortionist enters the battlefield or deals combat damage to a player, exile up to one target nonland permanent. + // For as long as that card remains exiled, its owner may cast it. + Ability exileAbility = new OrTriggeredAbility( + Zone.BATTLEFIELD, + new AerialExtortionistExileEffect(), + false, + "Whenever Aerial Extortionist enters the battlefield or deals combat damage to a player, ", + new EntersBattlefieldTriggeredAbility(null, false), + new DealsCombatDamageToAPlayerTriggeredAbility(null, false) + ); + exileAbility.addTarget(new TargetNonlandPermanent()); + this.addAbility(exileAbility); + + // Whenever another player casts a spell from anywhere other than their hand, draw a card. + this.addAbility(new SpellCastOpponentTriggeredAbility(new DrawCardSourceControllerEffect(1), false, true)); + } + + private AerialExtortionist(final AerialExtortionist card) { + super(card); + } + + @Override + public AerialExtortionist copy() { + return new AerialExtortionist(this); + } +} + +class AerialExtortionistExileEffect extends OneShotEffect { + + public AerialExtortionistExileEffect() { + super(Outcome.Benefit); + this.staticText = "exile up to one target nonland permanent. " + + "For as long as that card remains exiled, its owner may cast it"; + } + + public AerialExtortionistExileEffect(final AerialExtortionistExileEffect effect) { + super(effect); + } + + @Override + public AerialExtortionistExileEffect copy() { + return new AerialExtortionistExileEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent targetPermanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (controller == null || targetPermanent == null) { + return false; + } + + return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, targetPermanent, + TargetController.OWNER, Duration.Custom, false, false, true); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/AgentsToolkit.java b/Mage.Sets/src/mage/cards/a/AgentsToolkit.java new file mode 100644 index 0000000000..fbbe68580a --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AgentsToolkit.java @@ -0,0 +1,119 @@ +package mage.cards.a; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.counters.Counters; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.Set; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class AgentsToolkit extends CardImpl { + public AgentsToolkit(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{G}{U}"); + + this.subtype.add(SubType.CLUE); + + // Agent’s Toolkit enters the battlefield with a +1/+1 counter, a flying counter, a deathtouch counter, and a shield counter on it. + Ability counterETBAbility = new EntersBattlefieldAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance(1)).setText("with a +1/+1 counter")); + counterETBAbility.addEffect(new AddCountersSourceEffect(CounterType.FLYING.createInstance(1)).setText("a flying counter").concatBy(", ")); + counterETBAbility.addEffect(new AddCountersSourceEffect(CounterType.DEATHTOUCH.createInstance(1)).setText("a deathtouch counter").concatBy(", ")); + counterETBAbility.addEffect(new AddCountersSourceEffect(CounterType.SHIELD.createInstance(1)).setText("and a shield counter on it").concatBy(", ")); + this.addAbility(counterETBAbility); + + // Whenever a creature enters the battlefield under your control, + // you may move a counter from Agent’s Toolkit onto that creature. + this.addAbility(new EntersBattlefieldControlledTriggeredAbility( + new AgentToolkitMoveCounterEffect(), + StaticFilters.FILTER_PERMANENT_CREATURE) + ); + + // {2}, Sacrifice Agent’s Toolkit: Draw a card. + Ability drawAbility = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), new GenericManaCost(2)); + drawAbility.addCost(new SacrificeSourceCost()); + this.addAbility(drawAbility); + } + + private AgentsToolkit(final AgentsToolkit card) { + super(card); + } + + @Override + public AgentsToolkit copy() { + return new AgentsToolkit(this); + } +} + +class AgentToolkitMoveCounterEffect extends OneShotEffect { + + public AgentToolkitMoveCounterEffect() { + super(Outcome.Benefit); + this.staticText = "you may move a counter from Agent's Toolkit onto that creature"; + } + + private AgentToolkitMoveCounterEffect(final AgentToolkitMoveCounterEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent agentsToolkitPermanent = game.getPermanent(source.getSourceId()); + Player controller = game.getPlayer(source.getControllerId()); + if (agentsToolkitPermanent == null || controller == null) { + return false; + } + Object enteringObject = this.getValue("permanentEnteringBattlefield"); + if (!(enteringObject instanceof Permanent)) { + return false; + } + Permanent enteringCreature = (Permanent) enteringObject; + + Choice moveCounterChoice = new ChoiceImpl(false); + Set possibleCounterNames = agentsToolkitPermanent.getCounters(game).keySet(); + moveCounterChoice.setMessage("Choose counter to move"); + moveCounterChoice.setChoices(possibleCounterNames); + + if (controller.choose(outcome, moveCounterChoice, game) && possibleCounterNames.contains(moveCounterChoice.getChoice())) { + String counterName = moveCounterChoice.getChoice(); + CounterType counterType = CounterType.findByName(counterName); + if (counterType == null) { + return false; + } + agentsToolkitPermanent.removeCounters(counterType.getName(), 1, source, game); + enteringCreature.addCounters(counterType.createInstance(), source, game); + } + return true; + } + + @Override + public AgentToolkitMoveCounterEffect copy() { + return new AgentToolkitMoveCounterEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/AmuletOfSafekeeping.java b/Mage.Sets/src/mage/cards/a/AmuletOfSafekeeping.java index 92ff67533d..824eaade85 100644 --- a/Mage.Sets/src/mage/cards/a/AmuletOfSafekeeping.java +++ b/Mage.Sets/src/mage/cards/a/AmuletOfSafekeeping.java @@ -1,24 +1,18 @@ package mage.cards.a; import java.util.UUID; -import mage.abilities.TriggeredAbilityImpl; + +import mage.abilities.common.TargetOfOpponentsSpellOrAbilityTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.effects.Effect; import mage.abilities.effects.common.CounterUnlessPaysEffect; import mage.abilities.effects.common.continuous.BoostAllEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; -import mage.constants.TargetController; import mage.constants.Zone; -import mage.filter.FilterStackObject; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.stack.StackObject; -import mage.target.targetpointer.FixedTarget; /** * @@ -30,7 +24,7 @@ public final class AmuletOfSafekeeping extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}"); // Whenever you become the target of a spell or ability an opponent controls, counter that spell or ability unless its controller pays {1}. - this.addAbility(new AmuletOfSafekeepingTriggeredAbility()); + this.addAbility(new TargetOfOpponentsSpellOrAbilityTriggeredAbility(new CounterUnlessPaysEffect(new GenericManaCost(1)), Boolean.TRUE)); // Creature tokens get -1/-0. this.addAbility(new SimpleStaticAbility( @@ -51,49 +45,3 @@ public final class AmuletOfSafekeeping extends CardImpl { return new AmuletOfSafekeeping(this); } } - -class AmuletOfSafekeepingTriggeredAbility extends TriggeredAbilityImpl { - - private static final FilterStackObject filter = new FilterStackObject(); - - static { - filter.add(TargetController.OPPONENT.getControllerPredicate()); - } - - public AmuletOfSafekeepingTriggeredAbility() { - super(Zone.BATTLEFIELD, new CounterUnlessPaysEffect(new GenericManaCost(1))); - } - - public AmuletOfSafekeepingTriggeredAbility(final AmuletOfSafekeepingTriggeredAbility ability) { - super(ability); - } - - @Override - public AmuletOfSafekeepingTriggeredAbility copy() { - return new AmuletOfSafekeepingTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.TARGETED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - StackObject stackObject = game.getStack().getStackObject(event.getSourceId()); - if (event.getTargetId().equals(getControllerId()) - && filter.match(stackObject, getControllerId(), this, game)) { - for (Effect effect : this.getEffects()) { - effect.setTargetPointer(new FixedTarget(stackObject.getId(), game)); - } - return true; - } - return false; - } - - @Override - public String getRule() { - return "Whenever you become the target of a spell or ability an opponent controls, " - + "counter that spell or ability unless its controller pays {1}"; - } -} diff --git a/Mage.Sets/src/mage/cards/a/AnheloThePainter.java b/Mage.Sets/src/mage/cards/a/AnheloThePainter.java new file mode 100644 index 0000000000..007135bfb6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AnheloThePainter.java @@ -0,0 +1,140 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.keyword.CasualtyAbility; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; +import mage.players.Player; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class AnheloThePainter extends CardImpl { + public AnheloThePainter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{B}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.VAMPIRE, SubType.ASSASSIN); + this.power = new MageInt(1); + this.toughness = new MageInt(3); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // The first instant or sorcery spell you cast each turn has casualty 2. + this.addAbility( + new SimpleStaticAbility( + Zone.BATTLEFIELD, + new AnheloThePainterGainCausalityEffect()), + new AnheloThePainterWatcher() + ); + } + + private AnheloThePainter(final AnheloThePainter card) { + super(card); + } + + @Override + public AnheloThePainter copy() { + return new AnheloThePainter(this); + } +} + +class AnheloThePainterGainCausalityEffect extends ContinuousEffectImpl { + + AnheloThePainterGainCausalityEffect() { + super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + staticText = "The first instant or sorcery spell you cast each turn has casualty 2. " + + "(As you cast that spell, you may sacrifice a creature with power 2 or greater. " + + "When you do, copy the spell and you may choose new targets for the copy.)"; + } + + AnheloThePainterGainCausalityEffect(final AnheloThePainterGainCausalityEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + AnheloThePainterWatcher watcher = game.getState().getWatcher(AnheloThePainterWatcher.class); + if (controller == null || watcher == null) { + return false; + } + + boolean applied = false; + for (StackObject stackObject : game.getStack()) { + if (!(stackObject instanceof Spell) + || stackObject.isCopy() + || !stackObject.isControlledBy(source.getControllerId()) + || !AnheloThePainterWatcher.checkSpell(stackObject, game) ) { + continue; + } + Spell spell = (Spell) stackObject; + Card card = spell.getCard(); + game.getState().addOtherAbility(card, new CasualtyAbility(2)); + applied = true; + } + + return applied; + } + + @Override + public AnheloThePainterGainCausalityEffect copy() { + return new AnheloThePainterGainCausalityEffect(this); + } +} + +class AnheloThePainterWatcher extends Watcher { + + private final Map playerMap = new HashMap<>(); + + AnheloThePainterWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.CAST_SPELL) { + return; + } + + Spell spell = game.getSpell(event.getSourceId()); + if (spell == null || !spell.isInstantOrSorcery(game)) { + return; + } + playerMap.computeIfAbsent(event.getPlayerId(), x -> new MageObjectReference(spell.getMainCard(), game)); + } + + @Override + public void reset() { + playerMap.clear(); + super.reset(); + } + + static boolean checkSpell(StackObject stackObject, Game game) { + if (stackObject.isCopy() || !(stackObject instanceof Spell)) { + return false; + } + + UUID controllerId = stackObject.getControllerId(); + AnheloThePainterWatcher watcher = game.getState().getWatcher(AnheloThePainterWatcher.class); + return watcher.playerMap.containsKey(controllerId) + && watcher.playerMap.get(controllerId).refersTo(((Spell) stackObject).getMainCard(), game); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/a/AudaciousSwap.java b/Mage.Sets/src/mage/cards/a/AudaciousSwap.java index 73e6e205f8..d25818cd30 100644 --- a/Mage.Sets/src/mage/cards/a/AudaciousSwap.java +++ b/Mage.Sets/src/mage/cards/a/AudaciousSwap.java @@ -34,7 +34,7 @@ public final class AudaciousSwap extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{R}"); // Casualty 2 - this.addAbility(new CasualtyAbility(this, 2)); + this.addAbility(new CasualtyAbility(2)); // The owner of target nonenchantment permanent shuffles it into their library, then exiles the top card of their library. If it's a land card, they put it onto the battlefield. Otherwise, they may cast it without paying its mana cost. this.getSpellAbility().addEffect(new AudaciousSwapEffect()); diff --git a/Mage.Sets/src/mage/cards/b/BeseechTheQueen.java b/Mage.Sets/src/mage/cards/b/BeseechTheQueen.java index 608fa790dd..4ce848f2d6 100644 --- a/Mage.Sets/src/mage/cards/b/BeseechTheQueen.java +++ b/Mage.Sets/src/mage/cards/b/BeseechTheQueen.java @@ -3,14 +3,11 @@ package mage.cards.b; import java.util.UUID; import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; -import mage.filter.common.FilterControlledLandPermanent; -import mage.filter.predicate.Predicate; -import mage.game.Game; +import mage.filter.predicate.card.CardManaCostLessThanControlledLandCountPredicate; import mage.target.common.TargetCardInLibrary; /** @@ -20,9 +17,10 @@ import mage.target.common.TargetCardInLibrary; public final class BeseechTheQueen extends CardImpl { private static final FilterCard filter = new FilterCard("card with mana value less than or equal to the number of lands you control"); - static{ - filter.add(new BeseechTheQueenPredicate()); + static { + filter.add(CardManaCostLessThanControlledLandCountPredicate.getInstance()); } + public BeseechTheQueen(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{2/B}{2/B}{2/B}"); @@ -41,21 +39,3 @@ public final class BeseechTheQueen extends CardImpl { return new BeseechTheQueen(this); } } - - -class BeseechTheQueenPredicate implements Predicate { - - - @Override - public final boolean apply(Card input, Game game) { - if(input.getManaValue() <= game.getBattlefield().getAllActivePermanents(new FilterControlledLandPermanent(), input.getOwnerId(), game).size()){ - return true; - } - return false; - } - - @Override - public String toString() { - return "card with mana value less than or equal to the number of lands you control"; - } -} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/b/BessSoulNourisher.java b/Mage.Sets/src/mage/cards/b/BessSoulNourisher.java new file mode 100644 index 0000000000..5b19f72f73 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BessSoulNourisher.java @@ -0,0 +1,68 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.EntersBattlefieldOneOrMoreTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CountersSourceCount; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.*; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class BessSoulNourisher extends CardImpl { + + static final FilterCreaturePermanent filter = new FilterCreaturePermanent("one or more other creatures with base power and toughness 1/1"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(new BasePowerPredicate(ComparisonType.EQUAL_TO, 1)); + filter.add(new BaseToughnessPredicate(ComparisonType.EQUAL_TO, 1)); + } + + public BessSoulNourisher(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.HUMAN, SubType.CITIZEN); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Whenever one or more other creatures with base power and toughness 1/1 enter the battlefield under your control, + // put a +1/+1 counter on Bess, Soul Nourisher. + this.addAbility(new EntersBattlefieldOneOrMoreTriggeredAbility( + new AddCountersSourceEffect(CounterType.P1P1.createInstance()), + filter, + TargetController.YOU) + ); + + // Whenever Bess attacks, each other creature you control with base power and toughness 1/1 gets + // +X/+X until end of turn, where X is the number of +1/+1 counters on Bess. + DynamicValue xValue = new CountersSourceCount(CounterType.P1P1); + this.addAbility(new AttacksTriggeredAbility( + new BoostControlledEffect(xValue, xValue, Duration.EndOfTurn, StaticFilters.FILTER_PERMANENT_CREATURE, true), + false, + "whenever Bess attacks, each other creature you control with base power and toughness 1/1 " + + "gets +X/+X until end of turn, where X is the number of +1/+1 counters on Bess") + ); + } + + private BessSoulNourisher(final BessSoulNourisher card) { + super(card); + } + + @Override + public BessSoulNourisher copy() { + return new BessSoulNourisher(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CrypticPursuit.java b/Mage.Sets/src/mage/cards/c/CrypticPursuit.java new file mode 100644 index 0000000000..9c7542291b --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CrypticPursuit.java @@ -0,0 +1,107 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.asthought.PlayFromNotOwnHandZoneTargetEffect; +import mage.abilities.effects.keyword.ManifestEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.card.FaceDownPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class CrypticPursuit extends CardImpl { + + private static final FilterPermanent filterFaceDownPermanent = new FilterControlledCreaturePermanent("a face-down creature"); + static { + filterFaceDownPermanent.add(FaceDownPredicate.instance); + } + + public CrypticPursuit(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}{R}"); + + // Whenever you cast an instant or sorcery spell from your hand, manifest the top card of your library. + // (Put that card onto the battlefield face down as a 2/2 creature. + // Turn it face up any time for its mana cost if it’s a creature card.) + this.addAbility(new SpellCastControllerTriggeredAbility( + new ManifestEffect(1), + StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, + false, + Zone.HAND) + ); + + // Whenever a face-down creature you control dies, exile it if it’s an instant or sorcery card. + // You may cast that card until the end of your next turn. + this.addAbility(new DiesCreatureTriggeredAbility( + new CrypticPursuitExileAndPlayEffect(), + false, + filterFaceDownPermanent) + ); + } + + private CrypticPursuit(final CrypticPursuit card) { + super(card); + } + + @Override + public CrypticPursuit copy() { + return new CrypticPursuit(this); + } +} + +class CrypticPursuitExileAndPlayEffect extends OneShotEffect { + + CrypticPursuitExileAndPlayEffect() { + super(Outcome.Benefit); + this.staticText = "exile it if it's an instant or sorcery card. " + + "You may cast that card until the end of your next turn."; + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Object diedObject = getValue("creatureDied"); + if (controller == null || !(diedObject instanceof Permanent)) { + return false; + } + Permanent diedFaceDownCreature = (Permanent) diedObject; + + Card card = game.getCard(diedFaceDownCreature.getMainCard().getId()); + if (card == null || !card.isInstantOrSorcery(game)) { + return false; + } + + PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile( + game, source, card, TargetController.YOU, + Duration.UntilEndOfYourNextTurn, + false, false, true + ); + + return false; + } + + CrypticPursuitExileAndPlayEffect(final CrypticPursuitExileAndPlayEffect effect) { + super(effect); + } + + @Override + public Effect copy() { + return new CrypticPursuitExileAndPlayEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/c/CutOfTheProfits.java b/Mage.Sets/src/mage/cards/c/CutOfTheProfits.java index 7d8eba2b48..ff4d0cf76c 100644 --- a/Mage.Sets/src/mage/cards/c/CutOfTheProfits.java +++ b/Mage.Sets/src/mage/cards/c/CutOfTheProfits.java @@ -19,7 +19,7 @@ public final class CutOfTheProfits extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{X}{B}{B}"); // Casualty 3 - this.addAbility(new CasualtyAbility(this, 3)); + this.addAbility(new CasualtyAbility(3)); // You draw X cards and you lose X life. this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(ManacostVariableValue.REGULAR, "you")); diff --git a/Mage.Sets/src/mage/cards/c/CutYourLosses.java b/Mage.Sets/src/mage/cards/c/CutYourLosses.java index 544366fd4e..bb1af6fe81 100644 --- a/Mage.Sets/src/mage/cards/c/CutYourLosses.java +++ b/Mage.Sets/src/mage/cards/c/CutYourLosses.java @@ -18,7 +18,7 @@ public final class CutYourLosses extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{U}{U}"); // Casualty 2 - this.addAbility(new CasualtyAbility(this, 2)); + this.addAbility(new CasualtyAbility(2)); // Target player mills half their library, rounded down. this.getSpellAbility().addEffect(new MillHalfLibraryTargetEffect(false)); diff --git a/Mage.Sets/src/mage/cards/d/DakkonShadowSlayer.java b/Mage.Sets/src/mage/cards/d/DakkonShadowSlayer.java index 0d398ef26c..4a363f9ca4 100644 --- a/Mage.Sets/src/mage/cards/d/DakkonShadowSlayer.java +++ b/Mage.Sets/src/mage/cards/d/DakkonShadowSlayer.java @@ -4,22 +4,16 @@ import mage.abilities.Ability; import mage.abilities.LoyaltyAbility; import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.dynamicvalue.common.LandsYouControlCount; -import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.PutCardFromOneOfTwoZonesOntoBattlefieldEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; import mage.abilities.effects.keyword.SurveilEffect; import mage.abilities.hint.common.LandsYouControlHint; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.counters.CounterType; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.players.Player; -import mage.target.TargetCard; -import mage.target.common.TargetCardInGraveyard; -import mage.target.common.TargetCardInHand; import mage.target.common.TargetCreaturePermanent; import java.util.UUID; @@ -52,7 +46,7 @@ public final class DakkonShadowSlayer extends CardImpl { this.addAbility(ability); // −6: You may put an artifact card from your hand or graveyard onto the battlefield. - this.addAbility(new LoyaltyAbility(new DakkonShadowSlayerEffect(), -6)); + this.addAbility(new LoyaltyAbility(new PutCardFromOneOfTwoZonesOntoBattlefieldEffect(StaticFilters.FILTER_CARD_ARTIFACT), -6)); } private DakkonShadowSlayer(final DakkonShadowSlayer card) { @@ -64,45 +58,3 @@ public final class DakkonShadowSlayer extends CardImpl { return new DakkonShadowSlayer(this); } } - -class DakkonShadowSlayerEffect extends OneShotEffect { - - DakkonShadowSlayerEffect() { - super(Outcome.Benefit); - staticText = "you may put an artifact card from your hand or graveyard onto the battlefield"; - } - - private DakkonShadowSlayerEffect(final DakkonShadowSlayerEffect effect) { - super(effect); - } - - @Override - public DakkonShadowSlayerEffect copy() { - return new DakkonShadowSlayerEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - if (player == null) { - return false; - } - boolean inGrave = player.getGraveyard().count(StaticFilters.FILTER_CARD_ARTIFACT, game) > 0; - if (!inGrave && player.getHand().count(StaticFilters.FILTER_CARD_ARTIFACT, game) <1) { - return false; - } - TargetCard target; - if (!inGrave || player.chooseUse( - outcome, "Choose a card in your hand or your graveyard?", - null, "Hand", "Graveyard", source, game - )) { - target = new TargetCardInHand(0, 1, StaticFilters.FILTER_CARD_ARTIFACT); - player.choose(outcome, player.getHand(), target, game); - } else { - target = new TargetCardInGraveyard(0, 1, StaticFilters.FILTER_CARD_ARTIFACT); - player.choose(outcome, player.getGraveyard(), target, game); - } - Card card = game.getCard(target.getFirstTarget()); - return card != null && player.moveCards(card, Zone.BATTLEFIELD, source, game); - } -} diff --git a/Mage.Sets/src/mage/cards/d/DenryKlinEditorInChief.java b/Mage.Sets/src/mage/cards/d/DenryKlinEditorInChief.java new file mode 100644 index 0000000000..9b4b775291 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DenryKlinEditorInChief.java @@ -0,0 +1,98 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; +import mage.abilities.condition.common.SourceHasCountersCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.counter.AddCounterChoiceSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class DenryKlinEditorInChief extends CardImpl { + + public DenryKlinEditorInChief(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{U}"); + + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.CAT, SubType.ADVISOR); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Denry Klin, Editor in Chief enters the battlefield with your choice of a +1/+1, first strike, or vigilance counter on it. + this.addAbility(new EntersBattlefieldAbility( + new AddCounterChoiceSourceEffect(CounterType.P1P1, CounterType.FIRST_STRIKE, CounterType.VIGILANCE) + )); + + // Whenever a nontoken creature enters the battlefield under your control, + // if Denry has counters on it, put the same number of each kind of counter on that creature. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new EntersBattlefieldControlledTriggeredAbility( + new DenryKlinEditorInChiefCopyCountersEffect(), + StaticFilters.FILTER_CONTROLLED_CREATURE_NON_TOKEN), + SourceHasCountersCondition.instance, + "Whenever a nontoken creature enters the battlefield under your control, " + + "if Denry has counters on it, put the same number of each kind of counter on that creature.") + ); + + } + + private DenryKlinEditorInChief(final DenryKlinEditorInChief card) { + super(card); + } + + @Override + public DenryKlinEditorInChief copy() { + return new DenryKlinEditorInChief(this); + } +} + +class DenryKlinEditorInChiefCopyCountersEffect extends OneShotEffect { + + DenryKlinEditorInChiefCopyCountersEffect() { + super(Outcome.Benefit); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent denryPermanent = game.getPermanent(source.getSourceId()); + Object enteringObject = getValue("permanentEnteringBattlefield"); + if (controller == null || denryPermanent == null || !(enteringObject instanceof Permanent)) { + return false; + } + Permanent enteringCreature = (Permanent) enteringObject; + + for (Counter counter : denryPermanent.getCounters(game).values()) { + enteringCreature.addCounters(counter, source, game); + } + return true; + } + + private DenryKlinEditorInChiefCopyCountersEffect(final DenryKlinEditorInChiefCopyCountersEffect effect) { + super(effect); + } + + @Override + public Effect copy() { + return new DenryKlinEditorInChiefCopyCountersEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/d/DigUpTheBody.java b/Mage.Sets/src/mage/cards/d/DigUpTheBody.java index 28dbdad5e6..847f7e29be 100644 --- a/Mage.Sets/src/mage/cards/d/DigUpTheBody.java +++ b/Mage.Sets/src/mage/cards/d/DigUpTheBody.java @@ -26,7 +26,7 @@ public final class DigUpTheBody extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{B}"); // Casualty 1 - this.addAbility(new CasualtyAbility(this, 1)); + this.addAbility(new CasualtyAbility(1)); // Mill two cards, then return a creature card from your graveyard to your hand. this.getSpellAbility().addEffect(new DigUpTheBodyEffect()); diff --git a/Mage.Sets/src/mage/cards/d/DjinnIlluminatus.java b/Mage.Sets/src/mage/cards/d/DjinnIlluminatus.java index 703e9ed220..b6b55ec242 100644 --- a/Mage.Sets/src/mage/cards/d/DjinnIlluminatus.java +++ b/Mage.Sets/src/mage/cards/d/DjinnIlluminatus.java @@ -2,30 +2,23 @@ package mage.cards.d; import mage.MageInt; -import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.continuous.EachSpellYouCastHasReplicateEffect; import mage.abilities.keyword.FlyingAbility; -import mage.abilities.keyword.ReplicateAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.common.FilterInstantOrSorcerySpell; -import mage.game.Game; -import mage.game.stack.Spell; -import mage.game.stack.StackObject; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; -import mage.game.permanent.Permanent; /** - * * @author LevelX2 */ public final class DjinnIlluminatus extends CardImpl { + private static final FilterInstantOrSorcerySpell filter = new FilterInstantOrSorcerySpell(); + public DjinnIlluminatus(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{5}{U/R}{U/R}"); this.subtype.add(SubType.DJINN); @@ -36,8 +29,7 @@ public final class DjinnIlluminatus extends CardImpl { // Flying this.addAbility(FlyingAbility.getInstance()); // Each instant and sorcery spell you cast has replicate. The replicate cost is equal to its mana cost. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new DjinnIlluminatusGainReplicateEffect())); - + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new EachSpellYouCastHasReplicateEffect(filter))); } private DjinnIlluminatus(final DjinnIlluminatus card) { @@ -48,52 +40,4 @@ public final class DjinnIlluminatus extends CardImpl { public DjinnIlluminatus copy() { return new DjinnIlluminatus(this); } -} - -class DjinnIlluminatusGainReplicateEffect extends ContinuousEffectImpl { - - private static final FilterInstantOrSorcerySpell filter = new FilterInstantOrSorcerySpell(); - private final Map replicateAbilities = new HashMap<>(); - - public DjinnIlluminatusGainReplicateEffect() { - super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); - staticText = "Each instant and sorcery spell you cast has replicate. The replicate cost is equal to its mana cost " - + "(When you cast it, copy it for each time you paid its replicate cost. You may choose new targets for the copies.)"; - } - - public DjinnIlluminatusGainReplicateEffect(final DjinnIlluminatusGainReplicateEffect effect) { - super(effect); - this.replicateAbilities.putAll(effect.replicateAbilities); - } - - @Override - public DjinnIlluminatusGainReplicateEffect copy() { - return new DjinnIlluminatusGainReplicateEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Permanent djinn = game.getPermanent(source.getSourceId()); - if (djinn == null) { - return false; - } - for (StackObject stackObject : game.getStack()) { - // only spells cast, so no copies of spells - if ((stackObject instanceof Spell) - && !stackObject.isCopy() - && stackObject.isControlledBy(source.getControllerId()) - && djinn.isControlledBy(source.getControllerId()) // verify that the controller of the djinn cast that spell - && !stackObject.getManaCost().isEmpty()) { //handle cases like Ancestral Vision - Spell spell = (Spell) stackObject; - if (filter.match(stackObject, game)) { - ReplicateAbility replicateAbility = replicateAbilities.computeIfAbsent(spell.getId(), k -> new ReplicateAbility(spell.getSpellAbility().getManaCosts().getText())); - game.getState().addOtherAbility(spell.getCard(), replicateAbility, false); // Do not copy because paid and # of activations state is handled in the baility - } - } - } - if (game.getStack().isEmpty()) { - replicateAbilities.clear(); - } - return true; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/d/DonalHeraldOfWings.java b/Mage.Sets/src/mage/cards/d/DonalHeraldOfWings.java index 8751955fe3..433c6e39f2 100644 --- a/Mage.Sets/src/mage/cards/d/DonalHeraldOfWings.java +++ b/Mage.Sets/src/mage/cards/d/DonalHeraldOfWings.java @@ -38,8 +38,8 @@ public class DonalHeraldOfWings extends CardImpl { filterSpell.add(new AbilityPredicate(FlyingAbility.class)); } - public DonalHeraldOfWings(UUID ownderId, CardSetInfo setInfo) { - super(ownderId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{U}"); + public DonalHeraldOfWings(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{U}"); this.addSuperType(SuperType.LEGENDARY); diff --git a/Mage.Sets/src/mage/cards/f/FamilysFavor.java b/Mage.Sets/src/mage/cards/f/FamilysFavor.java new file mode 100644 index 0000000000..5596354008 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FamilysFavor.java @@ -0,0 +1,72 @@ +package mage.cards.f; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.common.AttacksWithCreaturesTriggeredAbility; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.costs.common.RemoveCountersSourceCost; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetAttackingCreature; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class FamilysFavor extends CardImpl { + + public FamilysFavor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); + + // Whenever you attack, put a shield counter on target attacking creature. + // Until end of turn, it gains + // “Whenever this creature deals combat damage to a player, + // remove a shield counter from it. + // If you do, draw a card.” + // (If a creature with a shield counter on it would be dealt damage or destroyed, remove a shield counter from it instead.) + Ability attacksAbility = new AttacksWithCreaturesTriggeredAbility(new AddCountersTargetEffect(CounterType.SHIELD.createInstance()), 1); + attacksAbility.addEffect(new GainAbilityTargetEffect( + new DealsCombatDamageToAPlayerTriggeredAbility( + new DoIfCostPaid( + new DrawCardSourceControllerEffect(1), + new RemoveCountersSourceCost(CounterType.SHIELD.createInstance())), + false), + Duration.EndOfTurn, + "Until end of turn, it gains " + + "\"Whenever this creature deals combat damage to a player, remove a shield counter from it. " + + "If you do, draw a card.\"" + ).setText("Until end of turn, it gains " + + "\"Whenever this creature deals combat damage to a player, remove a shield counter from it. " + + "If you do, draw a card.\" " + + "(If a creature with a shield counter on it would be dealt damage or destroyed, remove a shield counter from it instead.)" + ) + ); + + attacksAbility.addTarget(new TargetAttackingCreature()); + this.addAbility(attacksAbility); + } + + private FamilysFavor(final FamilysFavor card) { + super(card); + } + + @Override + public FamilysFavor copy() { + return new FamilysFavor(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FlawlessForgery.java b/Mage.Sets/src/mage/cards/f/FlawlessForgery.java index 0ded8c115d..7130541813 100644 --- a/Mage.Sets/src/mage/cards/f/FlawlessForgery.java +++ b/Mage.Sets/src/mage/cards/f/FlawlessForgery.java @@ -35,7 +35,7 @@ public final class FlawlessForgery extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{U}{U}"); // Casualty 3 - this.addAbility(new CasualtyAbility(this, 3)); + this.addAbility(new CasualtyAbility(3)); // Exile target instant or sorcery card from an opponent's graveyard. Copy that card. You may cast the copy without paying its mana cost. this.getSpellAbility().addEffect(new FlawlessForgeryEffect()); diff --git a/Mage.Sets/src/mage/cards/g/GiftOfImmortality.java b/Mage.Sets/src/mage/cards/g/GiftOfImmortality.java index c0ab9211c9..3b7b039905 100644 --- a/Mage.Sets/src/mage/cards/g/GiftOfImmortality.java +++ b/Mage.Sets/src/mage/cards/g/GiftOfImmortality.java @@ -8,6 +8,7 @@ import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbil import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.ReturnToBattlefieldAttachedEffect; import mage.abilities.keyword.EnchantAbility; import mage.cards.Card; import mage.cards.CardImpl; @@ -75,57 +76,25 @@ class GiftOfImmortalityEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Permanent enchantment = (Permanent) game.getLastKnownInformation(source.getSourceId(), Zone.BATTLEFIELD); Player controller = game.getPlayer(source.getControllerId()); - if (controller != null && enchantment != null && enchantment.getAttachedTo() != null) { - Permanent enchanted = (Permanent) game.getLastKnownInformation(enchantment.getAttachedTo(), Zone.BATTLEFIELD); - Card card = game.getCard(enchantment.getAttachedTo()); - if (card != null && enchanted != null && card.getZoneChangeCounter(game) == enchanted.getZoneChangeCounter(game) + 1) { - controller.moveCards(card, Zone.BATTLEFIELD, source, game, false, false, true, null); - Permanent permanent = game.getPermanent(card.getId()); - if (permanent != null) { - //create delayed triggered ability - Effect effect = new GiftOfImmortalityReturnEnchantmentEffect(); - effect.setTargetPointer(new FixedTarget(permanent, game)); - game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect), source); - } - - } - return true; + if (controller == null || enchantment == null || enchantment.getAttachedTo() == null) { + return false; + } + Permanent enchanted = (Permanent) game.getLastKnownInformation(enchantment.getAttachedTo(), Zone.BATTLEFIELD); + Card card = game.getCard(enchantment.getAttachedTo()); + if (card == null || enchanted == null || card.getZoneChangeCounter(game) != enchanted.getZoneChangeCounter(game) + 1) { + return false; } - return false; - } - -} - -class GiftOfImmortalityReturnEnchantmentEffect extends OneShotEffect { - - public GiftOfImmortalityReturnEnchantmentEffect() { - super(Outcome.PutCardInPlay); - staticText = "Return {this} to the battlefield attached to that creature at the beginning of the next end step"; - } - - public GiftOfImmortalityReturnEnchantmentEffect(final GiftOfImmortalityReturnEnchantmentEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - Card aura = game.getCard(source.getSourceId()); - if (aura != null && game.getState().getZone(aura.getId()) == Zone.GRAVEYARD) { - Player controller = game.getPlayer(source.getControllerId()); - Permanent creature = game.getPermanent(getTargetPointer().getFirst(game, source)); - if (controller != null && creature != null) { - game.getState().setValue("attachTo:" + aura.getId(), creature); - controller.moveCards(aura, Zone.BATTLEFIELD, source, game); - return creature.addAttachment(aura.getId(), source, game); - } + controller.moveCards(card, Zone.BATTLEFIELD, source, game, false, false, true, null); + Permanent permanent = game.getPermanent(card.getId()); + if (permanent == null) { + return false; } - return false; - } - - @Override - public GiftOfImmortalityReturnEnchantmentEffect copy() { - return new GiftOfImmortalityReturnEnchantmentEffect(this); + // Create delayed triggered ability + Effect effect = new ReturnToBattlefieldAttachedEffect(); + effect.setTargetPointer(new FixedTarget(permanent, game)); + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect), source); + return true; } } diff --git a/Mage.Sets/src/mage/cards/g/GrislySigil.java b/Mage.Sets/src/mage/cards/g/GrislySigil.java index 2d49c4e13c..0e5288e4ed 100644 --- a/Mage.Sets/src/mage/cards/g/GrislySigil.java +++ b/Mage.Sets/src/mage/cards/g/GrislySigil.java @@ -30,7 +30,7 @@ public final class GrislySigil extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{B}"); // Casualty 1 - this.addAbility(new CasualtyAbility(this, 1)); + this.addAbility(new CasualtyAbility(1)); // Choose target creature or planeswalker. If it was dealt noncombat damage this turn, Grisly Sigil deals 3 damage to it and you gain 3 life. Otherwise, Grisly Sigil deals 1 damage to it and you gain 1 life. this.getSpellAbility().addEffect(new GrislySigilEffect()); diff --git a/Mage.Sets/src/mage/cards/i/IllicitShipment.java b/Mage.Sets/src/mage/cards/i/IllicitShipment.java index aa92d727e9..0a68caf7df 100644 --- a/Mage.Sets/src/mage/cards/i/IllicitShipment.java +++ b/Mage.Sets/src/mage/cards/i/IllicitShipment.java @@ -18,7 +18,7 @@ public final class IllicitShipment extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}{B}"); // Casualty 3 - this.addAbility(new CasualtyAbility(this, 3)); + this.addAbility(new CasualtyAbility(3)); // Search your library for a card, put that card into your hand, then shuffle. this.getSpellAbility().addEffect(new SearchLibraryPutInHandEffect(new TargetCardInLibrary())); diff --git a/Mage.Sets/src/mage/cards/i/IronApprentice.java b/Mage.Sets/src/mage/cards/i/IronApprentice.java index fbc8996e26..08aec0180a 100644 --- a/Mage.Sets/src/mage/cards/i/IronApprentice.java +++ b/Mage.Sets/src/mage/cards/i/IronApprentice.java @@ -4,7 +4,7 @@ import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SourceHasCountersCondition; import mage.abilities.decorator.ConditionalTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; @@ -13,7 +13,6 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; -import mage.counters.Counter; import mage.counters.CounterType; import mage.game.Game; import mage.game.permanent.Permanent; @@ -40,7 +39,7 @@ public final class IronApprentice extends CardImpl { // When Iron Apprentice dies, if it had counters on it, put those counters on target creature you control. Ability ability = new ConditionalTriggeredAbility( - new DiesSourceTriggeredAbility(new IronApprenticeEffect()), IronApprenticeCondition.instance, + new DiesSourceTriggeredAbility(new IronApprenticeEffect()), SourceHasCountersCondition.instance, "When {this} dies, if it had counters on it, put those counters on target creature you control." ); ability.addTarget(new TargetControlledCreaturePermanent()); @@ -57,21 +56,6 @@ public final class IronApprentice extends CardImpl { } } -enum IronApprenticeCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - Permanent permanent = source.getSourcePermanentOrLKI(game); - return permanent != null && permanent - .getCounters(game) - .values() - .stream() - .mapToInt(Counter::getCount) - .anyMatch(x -> x > 0); - } -} - class IronApprenticeEffect extends OneShotEffect { IronApprenticeEffect() { @@ -98,7 +82,6 @@ class IronApprenticeEffect extends OneShotEffect { .getCounters(game) .copy() .values() - .stream() .forEach(counter -> creature.addCounters(counter, source, game)); return true; } diff --git a/Mage.Sets/src/mage/cards/j/JadziOracleOfArcavios.java b/Mage.Sets/src/mage/cards/j/JadziOracleOfArcavios.java index c613a9c8d5..4d207a4cf0 100644 --- a/Mage.Sets/src/mage/cards/j/JadziOracleOfArcavios.java +++ b/Mage.Sets/src/mage/cards/j/JadziOracleOfArcavios.java @@ -24,11 +24,7 @@ import mage.players.Player; import mage.target.common.TargetCardInHand; import java.util.UUID; -import mage.abilities.costs.Costs; -import mage.cards.AdventureCard; -import mage.cards.ModalDoubleFacesCardHalf; -import mage.cards.SplitCard; -import mage.cards.SplitCardHalf; +import mage.util.CardUtil; /** * @author TheElk801 @@ -120,96 +116,10 @@ class JadziOracleOfArcaviosEffect extends OneShotEffect { } // query player - if (!controller.chooseUse(outcome, "Cast " + card.getName() + " by paying {1}?", source, game)) { + if (!controller.chooseUse(outcome, "Cast revealed card " + card.getName() + " by paying {1}?", source, game)) { return false; } - - // handle split-cards - if (card instanceof SplitCard) { - SplitCardHalf leftHalfCard = ((SplitCard) card).getLeftHalfCard(); - SplitCardHalf rightHalfCard = ((SplitCard) card).getRightHalfCard(); - // get additional cost if any - Costs additionalCostsLeft = leftHalfCard.getSpellAbility().getCosts(); - Costs additionalCostsRight = rightHalfCard.getSpellAbility().getCosts(); - // set alternative cost and any additional cost - controller.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), new ManaCostsImpl<>("{1}"), additionalCostsLeft); - controller.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), new ManaCostsImpl<>("{1}"), additionalCostsRight); - // allow the card to be cast - game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), Boolean.TRUE); - game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), Boolean.TRUE); - } - - // handle MDFC - if (card instanceof ModalDoubleFacesCard) { - ModalDoubleFacesCardHalf leftHalfCard = ((ModalDoubleFacesCard) card).getLeftHalfCard(); - ModalDoubleFacesCardHalf rightHalfCard = ((ModalDoubleFacesCard) card).getRightHalfCard(); - // some MDFC cards are lands. IE: sea gate restoration - if (!leftHalfCard.isLand(game)) { - // get additional cost if any - Costs additionalCostsMDFCLeft = leftHalfCard.getSpellAbility().getCosts(); - // set alternative cost and any additional cost - controller.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), new ManaCostsImpl<>("{1}"), additionalCostsMDFCLeft); - } - if (!rightHalfCard.isLand(game)) { - // get additional cost if any - Costs additionalCostsMDFCRight = rightHalfCard.getSpellAbility().getCosts(); - // set alternative cost and any additional cost - controller.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), new ManaCostsImpl<>("{1}"), additionalCostsMDFCRight); - } - // allow the card to be cast - game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), Boolean.TRUE); - game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), Boolean.TRUE); - } - - // handle adventure cards - if (card instanceof AdventureCard) { - Card creatureCard = card.getMainCard(); - Card spellCard = ((AdventureCard) card).getSpellCard(); - // get additional cost if any - Costs additionalCostsCreature = creatureCard.getSpellAbility().getCosts(); - Costs additionalCostsSpellCard = spellCard.getSpellAbility().getCosts(); - // set alternative cost and any additional cost - controller.setCastSourceIdWithAlternateMana(creatureCard.getId(), new ManaCostsImpl<>("{1}"), additionalCostsCreature); - controller.setCastSourceIdWithAlternateMana(spellCard.getId(), new ManaCostsImpl<>("{1}"), additionalCostsSpellCard); - // allow the card to be cast - game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), Boolean.TRUE); - game.getState().setValue("PlayFromNotOwnHandZone" + spellCard.getId(), Boolean.TRUE); - } - - // normal card - if (!(card instanceof SplitCard) - || !(card instanceof ModalDoubleFacesCard) - || !(card instanceof AdventureCard)) { - // get additional cost if any - Costs additionalCostsNormalCard = card.getSpellAbility().getCosts(); - controller.setCastSourceIdWithAlternateMana(card.getMainCard().getId(), new ManaCostsImpl<>("{1}"), additionalCostsNormalCard); - } - - // cast it - controller.cast(controller.chooseAbilityForCast(card.getMainCard(), game, false), - game, false, new ApprovingObject(source, game)); - - // turn off effect after cast on every possible card-face - if (card instanceof SplitCard) { - SplitCardHalf leftHalfCard = ((SplitCard) card).getLeftHalfCard(); - SplitCardHalf rightHalfCard = ((SplitCard) card).getRightHalfCard(); - game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null); - game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null); - } - if (card instanceof ModalDoubleFacesCard) { - ModalDoubleFacesCardHalf leftHalfCard = ((ModalDoubleFacesCard) card).getLeftHalfCard(); - ModalDoubleFacesCardHalf rightHalfCard = ((ModalDoubleFacesCard) card).getRightHalfCard(); - game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null); - game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null); - } - if (card instanceof AdventureCard) { - Card creatureCard = card.getMainCard(); - Card spellCard = ((AdventureCard) card).getSpellCard(); - game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), null); - game.getState().setValue("PlayFromNotOwnHandZone" + spellCard.getId(), null); - } - // turn off effect on a normal card - game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + CardUtil.castSingle(controller, source, game, card, new ManaCostsImpl<>("{1}")); return true; } diff --git a/Mage.Sets/src/mage/cards/j/JoinTheMaestros.java b/Mage.Sets/src/mage/cards/j/JoinTheMaestros.java index 57768df143..359effdfb7 100644 --- a/Mage.Sets/src/mage/cards/j/JoinTheMaestros.java +++ b/Mage.Sets/src/mage/cards/j/JoinTheMaestros.java @@ -18,7 +18,7 @@ public final class JoinTheMaestros extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{B}"); // Casualty 2 - this.addAbility(new CasualtyAbility(this, 2)); + this.addAbility(new CasualtyAbility(2)); // Create a 4/3 black Ogre Warrior creature token. this.getSpellAbility().addEffect(new CreateTokenEffect(new OgreWarriorToken())); diff --git a/Mage.Sets/src/mage/cards/l/LeovoldEmissaryOfTrest.java b/Mage.Sets/src/mage/cards/l/LeovoldEmissaryOfTrest.java index 372ab84fa1..16ed6370e2 100644 --- a/Mage.Sets/src/mage/cards/l/LeovoldEmissaryOfTrest.java +++ b/Mage.Sets/src/mage/cards/l/LeovoldEmissaryOfTrest.java @@ -4,8 +4,8 @@ package mage.cards.l; import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.TargetOfOpponentsSpellOrAbilityTriggeredAbility; import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.cards.CardImpl; @@ -13,7 +13,6 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.permanent.Permanent; import mage.players.Player; import mage.watchers.common.CardsAmountDrawnThisTurnWatcher; @@ -35,7 +34,7 @@ public final class LeovoldEmissaryOfTrest extends CardImpl { this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new LeovoldEmissaryOfTrestEffect()), new CardsAmountDrawnThisTurnWatcher()); // Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, you may draw a card. - this.addAbility(new LeovoldEmissaryOfTrestTriggeredAbility()); + this.addAbility(new TargetOfOpponentsSpellOrAbilityTriggeredAbility(new DrawCardSourceControllerEffect(1), true)); } private LeovoldEmissaryOfTrest(final LeovoldEmissaryOfTrest card) { @@ -81,44 +80,4 @@ class LeovoldEmissaryOfTrestEffect extends ContinuousRuleModifyingEffectImpl { return watcher != null && controller != null && watcher.getAmountCardsDrawn(event.getPlayerId()) >= 1 && game.isOpponent(controller, event.getPlayerId()); } - -} - -class LeovoldEmissaryOfTrestTriggeredAbility extends TriggeredAbilityImpl { - - LeovoldEmissaryOfTrestTriggeredAbility() { - super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), true); - setTriggerPhrase("Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, "); - } - - LeovoldEmissaryOfTrestTriggeredAbility(final LeovoldEmissaryOfTrestTriggeredAbility ability) { - super(ability); - } - - @Override - public LeovoldEmissaryOfTrestTriggeredAbility copy() { - return new LeovoldEmissaryOfTrestTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.TARGETED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Player controller = game.getPlayer(this.getControllerId()); - Player targetter = game.getPlayer(event.getPlayerId()); - if (controller != null && targetter != null - && game.isOpponent(controller, targetter.getId())) { - if (event.getTargetId().equals(controller.getId())) { - return true; // Player was targeted - } - Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); - if (permanent != null && this.isControlledBy(permanent.getControllerId())) { - return true; - } - } - return false; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/l/LightEmUp.java b/Mage.Sets/src/mage/cards/l/LightEmUp.java index 3c3d094699..4ac7cd3164 100644 --- a/Mage.Sets/src/mage/cards/l/LightEmUp.java +++ b/Mage.Sets/src/mage/cards/l/LightEmUp.java @@ -18,7 +18,7 @@ public final class LightEmUp extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{R}"); // Casualty 2 - this.addAbility(new CasualtyAbility(this, 2)); + this.addAbility(new CasualtyAbility(2)); // Light 'Em Up deals 2 damage to target creature or planeswalker. this.getSpellAbility().addEffect(new DamageTargetEffect(2)); diff --git a/Mage.Sets/src/mage/cards/m/MakeDisappear.java b/Mage.Sets/src/mage/cards/m/MakeDisappear.java index b49f889815..cc83b1c703 100644 --- a/Mage.Sets/src/mage/cards/m/MakeDisappear.java +++ b/Mage.Sets/src/mage/cards/m/MakeDisappear.java @@ -19,7 +19,7 @@ public final class MakeDisappear extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{U}"); // Casualty 1 - this.addAbility(new CasualtyAbility(this, 1)); + this.addAbility(new CasualtyAbility(1)); // Counter target spell unless its controller pays {2}. this.getSpellAbility().addEffect(new CounterUnlessPaysEffect(new GenericManaCost(2))); diff --git a/Mage.Sets/src/mage/cards/m/MariTheKillingQuill.java b/Mage.Sets/src/mage/cards/m/MariTheKillingQuill.java new file mode 100644 index 0000000000..779ad8fddc --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MariTheKillingQuill.java @@ -0,0 +1,203 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.RemoveCounterCost; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.card.CastFromZonePredicate; +import mage.filter.predicate.card.FaceDownPredicate; +import mage.filter.predicate.card.OwnerIdPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentToken; +import mage.game.permanent.token.TreasureToken; +import mage.players.Player; +import mage.target.TargetCard; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class MariTheKillingQuill extends CardImpl { + + private static final FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent("Assassins, Mercenaries, and Rogues"); + static { + filter.add(Predicates.or( + SubType.ASSASSIN.getPredicate(), + SubType.MERCENARY.getPredicate(), + SubType.ROGUE.getPredicate() + )); + } + + public MariTheKillingQuill(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.VAMPIRE, SubType.ASSASSIN); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Whenever a creature an opponent controls dies, exile it with a hit counter on it. + this.addAbility(new SimpleStaticAbility(new MariTheKillingQuillReplacementEffect())); + + // Assassins, Mercenaries, and Rogues you control have deathtouch and + // "Whenever this creature deals combat damage to a player, you may remove a hit counter from a card that player owns in exile. + // If you do, draw a card and create two Treasure tokens." + GainAbilityControlledEffect gainDeathTouchEffect = new GainAbilityControlledEffect(DeathtouchAbility.getInstance(), Duration.WhileOnBattlefield, filter); + Ability mainAbility = new SimpleStaticAbility(Zone.BATTLEFIELD, gainDeathTouchEffect); + + // NOTE: Optional part is handled inside the effect + Ability dealsDamageAbility = new DealsCombatDamageToAPlayerTriggeredAbility(new MariTheKillingQuillDealsDamageEffect(), false, true); + Effect drawAndTreasureEffect = new GainAbilityControlledEffect(dealsDamageAbility, Duration.WhileOnBattlefield, filter); + drawAndTreasureEffect.setText( + "\"Whenever this creature deals combat damage to a player, you may remove a hit counter from a card taht player owns in exile. " + + "If you do, draw a card and create two Treasure tokens.\""); + drawAndTreasureEffect.concatBy("and"); + + mainAbility.addEffect(drawAndTreasureEffect); + + this.addAbility(mainAbility); + } + + private MariTheKillingQuill(final MariTheKillingQuill card) { + super(card); + } + + @Override + public MariTheKillingQuill copy() { + return new MariTheKillingQuill(this); + } +} + +class MariTheKillingQuillDealsDamageEffect extends OneShotEffect { + + MariTheKillingQuillDealsDamageEffect() { + super(Outcome.Benefit); + } + + private MariTheKillingQuillDealsDamageEffect(final MariTheKillingQuillDealsDamageEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Player opponent = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (controller == null || opponent == null) { + return false; + } + + FilterCard filterCard = new FilterCard("a card that player owns in exile"); + filterCard.add(new OwnerIdPredicate(opponent.getId())); + filterCard.add(Predicates.not(FaceDownPredicate.instance)); + filterCard.add(new CastFromZonePredicate(Zone.EXILED)); + + Effect doIfCostPaidEffect = new DoIfCostPaid( + new MariTheKillingQuillDrawAndTokenEffect(), + new RemoveCounterCost(new TargetCard(Zone.EXILED, filterCard)) + ); + + return doIfCostPaidEffect.apply(game, source); + } + + @Override + public MariTheKillingQuillDealsDamageEffect copy() { + return new MariTheKillingQuillDealsDamageEffect(this); + } +} + +class MariTheKillingQuillDrawAndTokenEffect extends OneShotEffect { + + MariTheKillingQuillDrawAndTokenEffect() { + super(Outcome.Benefit); + } + + private MariTheKillingQuillDrawAndTokenEffect(final MariTheKillingQuillDrawAndTokenEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + + Effect drawCardEffect = new DrawCardSourceControllerEffect(1); + Effect createTreasureEffect = new CreateTokenEffect(new TreasureToken(), 2); + + boolean success = drawCardEffect.apply(game, source); + success |= createTreasureEffect.apply(game, source); + + return success; + } + + @Override + public MariTheKillingQuillDrawAndTokenEffect copy() { + return new MariTheKillingQuillDrawAndTokenEffect(this); + } +} + +// Based on Draugr Necromancer +class MariTheKillingQuillReplacementEffect extends ReplacementEffectImpl { + + MariTheKillingQuillReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Exile); + staticText = "Whenever a creature an opponent controls dies, exile it with a hit counter on it."; + } + + private MariTheKillingQuillReplacementEffect(final MariTheKillingQuillReplacementEffect effect) { + super(effect); + } + + @Override + public MariTheKillingQuillReplacementEffect copy() { + return new MariTheKillingQuillReplacementEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Permanent permanent = ((ZoneChangeEvent) event).getTarget(); + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null + || permanent == null + || !controller.hasOpponent(permanent.getControllerId(), game)) { + return false; + } + + return CardUtil.moveCardWithCounter(game, source, controller, permanent, Zone.EXILED, CounterType.HIT.createInstance()); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + ZoneChangeEvent zce = (ZoneChangeEvent) event; + return zce.isDiesEvent() + && zce.getTarget().isCreature(game) + && !(zce.getTarget() instanceof PermanentToken); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/m/MasterOfCeremonies.java b/Mage.Sets/src/mage/cards/m/MasterOfCeremonies.java index 2f6e65bad1..c74eefb83f 100644 --- a/Mage.Sets/src/mage/cards/m/MasterOfCeremonies.java +++ b/Mage.Sets/src/mage/cards/m/MasterOfCeremonies.java @@ -28,8 +28,8 @@ import java.util.*; * @author Alex-Vasile */ public class MasterOfCeremonies extends CardImpl { - public MasterOfCeremonies(UUID ownderId, CardSetInfo setInfo) { - super(ownderId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + public MasterOfCeremonies(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); this.subtype.add(SubType.RHINO); this.subtype.add(SubType.DRUID); diff --git a/Mage.Sets/src/mage/cards/m/MastersRebuke.java b/Mage.Sets/src/mage/cards/m/MastersRebuke.java index 9a2f4323be..399b422760 100644 --- a/Mage.Sets/src/mage/cards/m/MastersRebuke.java +++ b/Mage.Sets/src/mage/cards/m/MastersRebuke.java @@ -23,8 +23,8 @@ public class MastersRebuke extends CardImpl { filter.add(TargetController.NOT_YOU.getControllerPredicate()); } - public MastersRebuke(UUID ownderId, CardSetInfo setInfo) { - super(ownderId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}"); + public MastersRebuke(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}"); // Target creature you control deals damage equal to its power to target creature or planeswalker you don’t control. this.getSpellAbility().addEffect(new DamageWithPowerFromOneToAnotherTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/m/MiragePhalanx.java b/Mage.Sets/src/mage/cards/m/MiragePhalanx.java index f7391909b6..d05765c594 100644 --- a/Mage.Sets/src/mage/cards/m/MiragePhalanx.java +++ b/Mage.Sets/src/mage/cards/m/MiragePhalanx.java @@ -29,8 +29,8 @@ public class MiragePhalanx extends CardImpl { "except it has haste and loses soulbond. " + "Exile it at end of combat.\""; - public MiragePhalanx(UUID ownderId, CardSetInfo setInfo) { - super(ownderId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}{R}"); + public MiragePhalanx(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}{R}"); this.addSubType(SubType.HUMAN); this.addSubType(SubType.SOLDIER); diff --git a/Mage.Sets/src/mage/cards/m/MyojinOfCrypticDreams.java b/Mage.Sets/src/mage/cards/m/MyojinOfCrypticDreams.java index ecae134392..a4414019dd 100644 --- a/Mage.Sets/src/mage/cards/m/MyojinOfCrypticDreams.java +++ b/Mage.Sets/src/mage/cards/m/MyojinOfCrypticDreams.java @@ -34,8 +34,8 @@ public class MyojinOfCrypticDreams extends CardImpl { permanentSpellFilter.add(PermanentPredicate.instance); } - public MyojinOfCrypticDreams(UUID ownderId, CardSetInfo setInfo) { - super(ownderId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{U}{U}{U}"); + public MyojinOfCrypticDreams(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{U}{U}{U}"); this.addSuperType(SuperType.LEGENDARY); this.subtype.add(SubType.SPIRIT); diff --git a/Mage.Sets/src/mage/cards/m/MyojinOfGrimBetrayal.java b/Mage.Sets/src/mage/cards/m/MyojinOfGrimBetrayal.java index 21cc2a0feb..5303ab0db3 100644 --- a/Mage.Sets/src/mage/cards/m/MyojinOfGrimBetrayal.java +++ b/Mage.Sets/src/mage/cards/m/MyojinOfGrimBetrayal.java @@ -37,8 +37,8 @@ public class MyojinOfGrimBetrayal extends CardImpl { private static final DynamicValue xValue = new CardsInAllGraveyardsCount(filter); private static final Hint hint = new ValueHint("Permanents put into the graveyard this turn", xValue); - public MyojinOfGrimBetrayal(UUID ownderId, CardSetInfo setInfo) { - super(ownderId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{B}{B}{B}"); + public MyojinOfGrimBetrayal(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{B}{B}{B}"); this.addSuperType(SuperType.LEGENDARY); this.subtype.add(SubType.SPIRIT); diff --git a/Mage.Sets/src/mage/cards/n/NahiriTheLithomancer.java b/Mage.Sets/src/mage/cards/n/NahiriTheLithomancer.java index 344805c873..c41a8f5474 100644 --- a/Mage.Sets/src/mage/cards/n/NahiriTheLithomancer.java +++ b/Mage.Sets/src/mage/cards/n/NahiriTheLithomancer.java @@ -8,14 +8,13 @@ import mage.abilities.common.CanBeYourCommanderAbility; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenEffect; -import mage.cards.Card; +import mage.abilities.effects.common.PutCardFromOneOfTwoZonesOntoBattlefieldEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.SuperType; -import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.common.FilterControlledPermanent; import mage.game.Game; @@ -25,8 +24,6 @@ import mage.game.permanent.token.NahiriTheLithomancerEquipmentToken; import mage.game.permanent.token.Token; import mage.players.Player; import mage.target.Target; -import mage.target.common.TargetCardInHand; -import mage.target.common.TargetCardInYourGraveyard; import mage.target.common.TargetControlledPermanent; /** @@ -35,6 +32,12 @@ import mage.target.common.TargetControlledPermanent; */ public final class NahiriTheLithomancer extends CardImpl { + private static final FilterCard filter = new FilterCard("an Equipment"); + + static { + filter.add(SubType.EQUIPMENT.getPredicate()); + } + public NahiriTheLithomancer(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{3}{W}{W}"); this.addSuperType(SuperType.LEGENDARY); @@ -46,7 +49,7 @@ public final class NahiriTheLithomancer extends CardImpl { this.addAbility(new LoyaltyAbility(new NahiriTheLithomancerFirstAbilityEffect(), 2)); // -2: You may put an Equipment card from your hand or graveyard onto the battlefield. - this.addAbility(new LoyaltyAbility(new NahiriTheLithomancerSecondAbilityEffect(), -2)); + this.addAbility(new LoyaltyAbility(new PutCardFromOneOfTwoZonesOntoBattlefieldEffect(filter), -2)); // -10: Create a colorless Equipment artifact token named Stoneforged Blade. It has indestructible, "Equipped creature gets +5/+5 and has double strike," and equip {0}. Effect effect = new CreateTokenEffect(new NahiriTheLithomancerEquipmentToken()); @@ -122,50 +125,3 @@ class NahiriTheLithomancerFirstAbilityEffect extends OneShotEffect { return false; } } - -class NahiriTheLithomancerSecondAbilityEffect extends OneShotEffect { - - private static final FilterCard filter = new FilterCard("an Equipment"); - - static { - filter.add(SubType.EQUIPMENT.getPredicate()); - } - - NahiriTheLithomancerSecondAbilityEffect() { - super(Outcome.PutCardInPlay); - this.staticText = "You may put an Equipment card from your hand or graveyard onto the battlefield"; - } - - NahiriTheLithomancerSecondAbilityEffect(final NahiriTheLithomancerSecondAbilityEffect effect) { - super(effect); - } - - @Override - public NahiriTheLithomancerSecondAbilityEffect copy() { - return new NahiriTheLithomancerSecondAbilityEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - if (controller.chooseUse(Outcome.PutCardInPlay, "Put an Equipment from hand? (No = from graveyard)", source, game)) { - Target target = new TargetCardInHand(0, 1, filter); - controller.choose(outcome, target, source, game); - Card card = controller.getHand().get(target.getFirstTarget(), game); - if (card != null) { - controller.moveCards(card, Zone.BATTLEFIELD, source, game); - } - } else { - Target target = new TargetCardInYourGraveyard(0, 1, filter); - target.choose(Outcome.PutCardInPlay, source.getControllerId(), source.getSourceId(), source, game); - Card card = controller.getGraveyard().get(target.getFirstTarget(), game); - if (card != null) { - controller.moveCards(card, Zone.BATTLEFIELD, source, game); - } - } - return true; - } - return false; - } -} diff --git a/Mage.Sets/src/mage/cards/n/NextOfKin.java b/Mage.Sets/src/mage/cards/n/NextOfKin.java new file mode 100644 index 0000000000..b11f709af5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NextOfKin.java @@ -0,0 +1,106 @@ +package mage.cards.n; + +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.common.DiesAttachedTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.effects.common.InfoEffect; +import mage.abilities.effects.common.PutCardFromOneOfTwoZonesOntoBattlefieldEffect; +import mage.abilities.effects.common.ReturnToBattlefieldAttachedEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.card.CastFromZonePredicate; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.TargetPermanent; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author Alex-Vasile + */ +public class NextOfKin extends CardImpl { + + public NextOfKin(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{G}"); + this.addSubType(SubType.AURA); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.Benefit)); + this.addAbility(new EnchantAbility(auraTarget.getTargetName())); + + // When enchanted creature dies, you may put a creature card you own with lesser mana value from your hand or from the command zone onto the battlefield. + // If you do, return Next of Kin to the battlefield attached to that creature at the beginning of the next end step. + this.addAbility(new DiesAttachedTriggeredAbility(new NextOfKinDiesEffect(), "enchanted creature", true)); + } + + private NextOfKin(final NextOfKin card) { + super(card); + } + + @Override + public NextOfKin copy() { + return new NextOfKin(this); + } +} + +class NextOfKinDiesEffect extends OneShotEffect { + + NextOfKinDiesEffect() { + super(Outcome.Benefit); + this.staticText = "you may put a creature card you own with lesser mana value from your hand or from the command zone onto the battlefield." + + "If you do, return Next of Kin to the battlefield attached to that creature at the beginning of the next end step."; + } + + private NextOfKinDiesEffect(final NextOfKinDiesEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Card nextOfKinCard = (Card) source.getSourceObjectIfItStillExists(game); + Object object = getValue("attachedTo"); + if (controller == null || nextOfKinCard == null || !(object instanceof Permanent)) { + return false; + } + int manaValue = ((Permanent) object).getManaValue(); + + FilterCreatureCard filterCreatureCard = new FilterCreatureCard("a creature card you own with lesser mana value from your hand or from the command zone"); + filterCreatureCard.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, manaValue)); + + // This effect is used only to get the info about which card was added. + Effect hackTargetEffect = new InfoEffect(""); + + Effect putCardEffect = new PutCardFromOneOfTwoZonesOntoBattlefieldEffect(filterCreatureCard, false, hackTargetEffect, Zone.HAND, Zone.COMMAND); + boolean cardPut = putCardEffect.apply(game, source); + if (!cardPut) { + return false; + } + + Effect returnToBattlefieldAttachedEffect = new ReturnToBattlefieldAttachedEffect(); + returnToBattlefieldAttachedEffect.setTargetPointer(hackTargetEffect.getTargetPointer()); + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(returnToBattlefieldAttachedEffect), source); + return true; + } + + @Override + public NextOfKinDiesEffect copy() { + return new NextOfKinDiesEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/n/NissaOfShadowedBoughs.java b/Mage.Sets/src/mage/cards/n/NissaOfShadowedBoughs.java index a2d85dbe5f..1f51c9107e 100644 --- a/Mage.Sets/src/mage/cards/n/NissaOfShadowedBoughs.java +++ b/Mage.Sets/src/mage/cards/n/NissaOfShadowedBoughs.java @@ -3,29 +3,26 @@ package mage.cards.n; import mage.abilities.Ability; import mage.abilities.LoyaltyAbility; import mage.abilities.common.LandfallAbility; +import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.PutCardFromOneOfTwoZonesOntoBattlefieldEffect; import mage.abilities.effects.common.UntapTargetEffect; import mage.abilities.effects.common.continuous.BecomesCreatureTargetEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; import mage.abilities.keyword.HasteAbility; import mage.abilities.keyword.MenaceAbility; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.counters.CounterType; import mage.filter.FilterCard; import mage.filter.StaticFilters; -import mage.filter.common.FilterCreatureCard; -import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.filter.predicate.card.CardManaCostLessThanControlledLandCountPredicate; import mage.game.Game; -import mage.game.permanent.Permanent; import mage.game.permanent.token.custom.CreatureToken; import mage.players.Player; -import mage.target.TargetCard; import mage.target.TargetPermanent; -import mage.target.common.TargetCardInHand; -import mage.target.common.TargetCardInYourGraveyard; import java.util.UUID; @@ -36,6 +33,11 @@ import static mage.constants.Outcome.Benefit; */ public final class NissaOfShadowedBoughs extends CardImpl { + private static final FilterCard filter = new FilterCard("card with mana value less than or equal to the number of lands you control"); + static { + filter.add(CardManaCostLessThanControlledLandCountPredicate.getInstance()); + } + public NissaOfShadowedBoughs(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{B}{G}"); @@ -52,8 +54,13 @@ public final class NissaOfShadowedBoughs extends CardImpl { ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND)); this.addAbility(ability); - // −5: You may put a creature card with converted mana cost less than or equal to the number of lands you control onto the battlefield from your hand or graveyard with two +1/+1 counters on it. - this.addAbility(new LoyaltyAbility(new NissaOfShadowedBoughsCreatureEffect(), -5)); + // −5: You may put a creature card with mana value less than or equal to the number of lands you control onto the battlefield from your hand or graveyard with two +1/+1 counters on it. + Effect putCardEffect = new PutCardFromOneOfTwoZonesOntoBattlefieldEffect(filter, false, new AddCountersTargetEffect(CounterType.P1P1.createInstance(2))); + putCardEffect.setText("You may put a creature card with mana value less than or equal to " + + "the number of lands you control onto the battlefield from your hand or graveyard " + + "with two +1/+1 counters on it."); + this.addAbility(new LoyaltyAbility(putCardEffect,-5) + ); } private NissaOfShadowedBoughs(final NissaOfShadowedBoughs card) { @@ -99,61 +106,3 @@ class NissaOfShadowedBoughsLandEffect extends OneShotEffect { return true; } } - -class NissaOfShadowedBoughsCreatureEffect extends OneShotEffect { - - NissaOfShadowedBoughsCreatureEffect() { - super(Outcome.Benefit); - staticText = "You may put a creature card with mana value less than or equal to " + - "the number of lands you control onto the battlefield from your hand or graveyard " + - "with two +1/+1 counters on it."; - } - - private NissaOfShadowedBoughsCreatureEffect(final NissaOfShadowedBoughsCreatureEffect effect) { - super(effect); - } - - @Override - public NissaOfShadowedBoughsCreatureEffect copy() { - return new NissaOfShadowedBoughsCreatureEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - if (player == null) { - return false; - } - int lands = game.getBattlefield().count( - StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND, - source.getControllerId(), source, game - ); - FilterCard filter = new FilterCreatureCard("creature card with mana value " + lands + " or less"); - filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, lands + 1)); - int inHand = player.getHand().count(filter, game); - int inGrave = player.getGraveyard().count(filter, game); - if (inHand < 1 && inGrave < 1) { - return false; - } - TargetCard target; - if (inHand < 1 || (inGrave > 0 && !player.chooseUse( - outcome, "Put a creature card from your hand or graveyard onto the battlefield?", - null, "Hand", "Graveyard", source, game - ))) { - target = new TargetCardInYourGraveyard(0, 1, filter, true); - } else { - target = new TargetCardInHand(filter); - } - player.choose(outcome, target, source, game); - Card card = game.getCard(target.getFirstTarget()); - if (card == null) { - return false; - } - player.moveCards(card, Zone.BATTLEFIELD, source, game); - Permanent permanent = game.getPermanent(card.getId()); - if (permanent != null) { - permanent.addCounters(CounterType.P1P1.createInstance(2), source.getControllerId(), source, game); - } - return true; - } -} diff --git a/Mage.Sets/src/mage/cards/o/ObNixilisTheAdversary.java b/Mage.Sets/src/mage/cards/o/ObNixilisTheAdversary.java index 70d368c792..921b042e1c 100644 --- a/Mage.Sets/src/mage/cards/o/ObNixilisTheAdversary.java +++ b/Mage.Sets/src/mage/cards/o/ObNixilisTheAdversary.java @@ -69,6 +69,7 @@ public final class ObNixilisTheAdversary extends CardImpl { } } +// TODO: Would be nice to refactor into Casualty ability so this doesn't have a custom implementation to maintain class ObNixilisTheAdversaryCasualtyAbility extends StaticAbility { public ObNixilisTheAdversaryCasualtyAbility(Card card) { diff --git a/Mage.Sets/src/mage/cards/o/OskarRubbishReclaimer.java b/Mage.Sets/src/mage/cards/o/OskarRubbishReclaimer.java new file mode 100644 index 0000000000..3144a90fbb --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OskarRubbishReclaimer.java @@ -0,0 +1,120 @@ +package mage.cards.o; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DiscardCardControllerTriggeredAbility; +import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; +import mage.abilities.hint.ValueHint; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class OskarRubbishReclaimer extends CardImpl { + + private static final ValueHint hint = new ValueHint("Number of different mana values in your graveyard", OskarRubbishReclaimerValue.instance); + + public OskarRubbishReclaimer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{B}"); + + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.HUMAN, SubType.WIZARD); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // This spell costs {1} less to cast for each different mana value among cards in your graveyard. + Effect spellReductionEffect = new SpellCostReductionForEachSourceEffect(1, OskarRubbishReclaimerValue.instance); + this.addAbility(new SimpleStaticAbility(Zone.ALL, spellReductionEffect).addHint(hint)); + + // Whenever you discard a nonland card, you may cast it from your graveyard. + // Optinal part is handled by the effect + this.addAbility(new DiscardCardControllerTriggeredAbility(new OskarRubbishReclaimerCastEffect(), false, StaticFilters.FILTER_CARD_A_NON_LAND)); + } + + private OskarRubbishReclaimer(final OskarRubbishReclaimer card) { + super(card); + } + + @Override + public OskarRubbishReclaimer copy() { + return new OskarRubbishReclaimer(this); + } +} + +class OskarRubbishReclaimerCastEffect extends OneShotEffect { + OskarRubbishReclaimerCastEffect() { + super(Outcome.Benefit); + this.staticText = "you may cast it from your graveyard."; + } + + private OskarRubbishReclaimerCastEffect(final OskarRubbishReclaimerCastEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Card card = (Card) getValue("discardedCard"); + if (controller == null || card == null || game.getState().getZone(card.getId()) != Zone.GRAVEYARD) { + return false; + } + + if (!controller.chooseUse(Outcome.Benefit, "Cast " + card.getName() + "?", source, game)) { + return false; + } + CardUtil.castSingle(controller, source, game, card); + + return true; + } + + @Override + public OskarRubbishReclaimerCastEffect copy() { + return new OskarRubbishReclaimerCastEffect(this); + } +} + +enum OskarRubbishReclaimerValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + Player player = game.getPlayer(sourceAbility.getControllerId()); + return player == null ? 0 : player + .getGraveyard() + .getCards(game) + .stream() + .map(MageObject::getManaValue) + .distinct() + .mapToInt(x -> 1) + .sum(); + } + + @Override + public OskarRubbishReclaimerValue copy() { + return this; + } + + @Override + public String getMessage() { + return "different mana value among cards in your graveyard"; + } + + @Override + public String toString() { + return "1"; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/ParnesseTheSubtleBrush.java b/Mage.Sets/src/mage/cards/p/ParnesseTheSubtleBrush.java new file mode 100644 index 0000000000..16810ce379 --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/ParnesseTheSubtleBrush.java @@ -0,0 +1,118 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.TargetOfOpponentsSpellOrAbilityTriggeredAbility; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CounterUnlessPaysEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.players.Player; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class ParnesseTheSubtleBrush extends CardImpl { + + protected static final String SPELL_KEY = "castCopiedSpell"; + + public ParnesseTheSubtleBrush(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{B}{R}"); + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.VAMPIRE, SubType.WIZARD); + + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, + // counter that spell or ability unless that player pays 4 life. + this.addAbility(new TargetOfOpponentsSpellOrAbilityTriggeredAbility(new CounterUnlessPaysEffect(new PayLifeCost(4).setText("4 life")))); + + // Whenever you copy a spell, up to one target opponent may also copy that spell. + // They may choose new targets for that copy. + this.addAbility(new ParnesseTheSubtleBrushCopySpellTriggeredAbility()); + } + + private ParnesseTheSubtleBrush(final ParnesseTheSubtleBrush card) { + super(card); + } + + @Override + public ParnesseTheSubtleBrush copy() { + return new ParnesseTheSubtleBrush(this); + } +} + +class ParnesseTheSubtleBrushCopySpellOpponentEffect extends OneShotEffect { + + ParnesseTheSubtleBrushCopySpellOpponentEffect() { + super(Outcome.Detriment); + this.staticText = "up to one target opponent may also copy that spell. " + + "They may choose new targets for that copy"; + } + + ParnesseTheSubtleBrushCopySpellOpponentEffect(final ParnesseTheSubtleBrushCopySpellOpponentEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player opponent = game.getPlayer(source.getFirstTarget()); + Object object = getValue(ParnesseTheSubtleBrush.SPELL_KEY); + if (opponent == null || !(object instanceof Spell)) { + return false; + } + Spell spellToCopy = (Spell) object; + spellToCopy.createCopyOnStack(game, source, opponent.getId(), true); + return true; + } + + @Override + public Effect copy() { + return new ParnesseTheSubtleBrushCopySpellOpponentEffect(this); + } +} + +class ParnesseTheSubtleBrushCopySpellTriggeredAbility extends TriggeredAbilityImpl { + + ParnesseTheSubtleBrushCopySpellTriggeredAbility() { + super(Zone.BATTLEFIELD, new ParnesseTheSubtleBrushCopySpellOpponentEffect(), true); + this.getTargets().add(new TargetOpponent(0, 1, false)); + } + + private ParnesseTheSubtleBrushCopySpellTriggeredAbility(final ParnesseTheSubtleBrushCopySpellTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.COPIED_STACKOBJECT; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Spell spell = game.getSpell(event.getTargetId()); + if (spell == null || !spell.isControlledBy(this.getControllerId())) { + return false; + } + getEffects().setValue(ParnesseTheSubtleBrush.SPELL_KEY, spell); + return true; + } + + @Override + public TriggeredAbility copy() { + return new ParnesseTheSubtleBrushCopySpellTriggeredAbility(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/p/PhabineBosssConfidant.java b/Mage.Sets/src/mage/cards/p/PhabineBosssConfidant.java new file mode 100644 index 0000000000..65ac9fc98f --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PhabineBosssConfidant.java @@ -0,0 +1,124 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfCombatTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.ParleyCount; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardAllEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.permanent.TokenPredicate; +import mage.game.Game; +import mage.game.permanent.token.CitizenGreenWhiteToken; +import mage.game.permanent.token.Token; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class PhabineBosssConfidant extends CardImpl { + + private static final FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent("Creature tokens you control"); + static { + filter.add(TokenPredicate.TRUE); + } + + public PhabineBosssConfidant(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}{G}{W}"); + + addSuperType(SuperType.LEGENDARY); + addSubType(SubType.CAT, SubType.ADVISOR); + this.power = new MageInt(3); + this.toughness = new MageInt(6); + + // Creature tokens you control have haste. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + HasteAbility.getInstance(), + Duration.WhileOnBattlefield, + filter) + )); + + // Parley — At the beginning of combat on your turn, each player reveals the top card of their library. + // For each land card revealed this way, you create a 1/1 green and white Citizen creature token. + // Then creatures you control get +1/+1 until end of turn for each nonland card revealed this way. + // Then each player draws a card. + Ability parleyAbility = new BeginningOfCombatTriggeredAbility( + new PhabineBosssConfidantParleyEffect(), + TargetController.YOU, + false + ); + Effect drawCardAllEffect = new DrawCardAllEffect(1); + drawCardAllEffect.concatBy("Then"); + parleyAbility.addEffect(drawCardAllEffect); + parleyAbility.setAbilityWord(AbilityWord.PARLEY); + this.addAbility(parleyAbility); + } + + private PhabineBosssConfidant(final PhabineBosssConfidant card) { + super(card); + } + + @Override + public PhabineBosssConfidant copy() { + return new PhabineBosssConfidant(this); + } +} + +class PhabineBosssConfidantParleyEffect extends OneShotEffect { + + PhabineBosssConfidantParleyEffect() { + super(Outcome.Benefit); + this.staticText = "each player reveals the top card of their library. " + + "For each land card revealed this way, you create a 1/1 green and white Citizen creature token. " + + "Then creatures you control get +1/+1 until end of turn for each nonland card revealed this way."; + } + + private PhabineBosssConfidantParleyEffect(final PhabineBosssConfidantParleyEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + + int landCount = ParleyCount.getInstance().calculate(game, source, this); + int nonEmptyLibraries = 0; + for (UUID playerID : game.getState().getPlayersInRange(controller.getId(), game)) { + Player player = game.getPlayer(playerID); + if (player != null && player.getLibrary().size() != 0) { + nonEmptyLibraries++; + } + } + int nonLandCount = nonEmptyLibraries - landCount; + + if (landCount > 0) { + Token citizenToken = new CitizenGreenWhiteToken(); + citizenToken.putOntoBattlefield(landCount, game, source, source.getControllerId(), false, false); + } + + if (nonLandCount > 0) { + Effect boostEffect = new BoostControlledEffect(nonLandCount, nonLandCount, Duration.EndOfTurn); + boostEffect.apply(game, source); + } + + return true; + } + + @Override + public PhabineBosssConfidantParleyEffect copy() { + return new PhabineBosssConfidantParleyEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RainOfRiches.java b/Mage.Sets/src/mage/cards/r/RainOfRiches.java new file mode 100644 index 0000000000..7f3daec505 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RainOfRiches.java @@ -0,0 +1,160 @@ +package mage.cards.r; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.keyword.CascadeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.token.TreasureToken; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; +import mage.players.Player; +import mage.watchers.Watcher; +import mage.watchers.common.ManaPaidSourceWatcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class RainOfRiches extends CardImpl { + + public RainOfRiches(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{R}{R}"); + + // When Rain of Riches enters the battlefield, create two Treasure tokens. + this.addAbility(new EntersBattlefieldTriggeredAbility(new CreateTokenEffect(new TreasureToken(), 2))); + + // The first spell you cast each turn that mana from a Treasure was spent to cast has cascade. + // (When you cast the spell, exile cards from the top of your library until you exile a nonland card that costs less. + // You may cast it without paying its mana cost. + // Put the exiled cards on the bottom of your library in a random order.) + this.addAbility( + new SimpleStaticAbility(Zone.BATTLEFIELD, new RainOfRichesGainsCascadeEffect()), + new RainOfRichesWatcher() + ); + } + + private RainOfRiches(final RainOfRiches card) { + super(card); + } + + @Override + public RainOfRiches copy() { + return new RainOfRiches(this); + } +} + +class RainOfRichesGainsCascadeEffect extends ContinuousEffectImpl { + + private final Ability cascadeAbility = new CascadeAbility(); + + RainOfRichesGainsCascadeEffect() { + super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.staticText = + "The first spell you cast each turn that mana from a Treasure was spent to cast has cascade. " + + "(When you cast the spell, exile cards from the top of your library until you exile a nonland card that costs less. " + + "You may cast it without paying its mana cost. " + + "Put the exiled cards on the bottom of your library in a random order.)"; + } + + private RainOfRichesGainsCascadeEffect(final RainOfRichesGainsCascadeEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class); + if (controller == null || watcher == null) { + return false; + } + + for (StackObject stackObject : game.getStack()) { + // Only spells cast, so no copies of spells + if ((stackObject instanceof Spell) + && !stackObject.isCopy() + && stackObject.isControlledBy(source.getControllerId())) { + Spell spell = (Spell) stackObject; + + if (FirstSpellCastWithTreasureCondition.instance.apply(game, source)) { + game.getState().addOtherAbility(spell.getCard(), cascadeAbility); + return true; // TODO: I think this should return here as soon as it finds the first one. + // If it should, change WildMageSorcerer to also return early. + } + } + } + return false; + } + + @Override + public RainOfRichesGainsCascadeEffect copy() { + return new RainOfRichesGainsCascadeEffect(this); + } +} + +enum FirstSpellCastWithTreasureCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + if (game.getStack().isEmpty()) { + return false; + } + RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class); + StackObject so = game.getStack().getFirst(); + return watcher != null && RainOfRichesWatcher.checkSpell(so, game); + } +} + +class RainOfRichesWatcher extends Watcher { + + private final Map playerMap = new HashMap<>(); + + RainOfRichesWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.CAST_SPELL) { + return; + } + Spell spell = game.getSpell(event.getSourceId()); + if (spell == null) { + return; + } + int manaPaid = ManaPaidSourceWatcher.getTreasurePaid(spell.getId(), game); + if (manaPaid < 1) { + return; + } + + playerMap.computeIfAbsent(event.getPlayerId(), x -> new MageObjectReference(spell.getMainCard(), game)); + } + + @Override + public void reset() { + playerMap.clear(); + super.reset(); + } + + static boolean checkSpell(StackObject stackObject, Game game) { + if (stackObject.isCopy() + || !(stackObject instanceof Spell)) { + return false; + } + RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class); + return watcher.playerMap.containsKey(stackObject.getControllerId()) + && watcher.playerMap.get(stackObject.getControllerId()).refersTo(((Spell) stackObject).getMainCard(), game); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RayneAcademyChancellor.java b/Mage.Sets/src/mage/cards/r/RayneAcademyChancellor.java index 7f68e6d864..5f7a50d4c1 100644 --- a/Mage.Sets/src/mage/cards/r/RayneAcademyChancellor.java +++ b/Mage.Sets/src/mage/cards/r/RayneAcademyChancellor.java @@ -3,21 +3,16 @@ package mage.cards.r; import java.util.UUID; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.TargetOfOpponentsSpellOrAbilityTriggeredAbility; import mage.abilities.condition.common.EnchantedSourceCondition; import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.Effect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.SuperType; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.GameEvent.EventType; -import mage.game.permanent.Permanent; -import mage.players.Player; /** * @@ -34,8 +29,15 @@ public final class RayneAcademyChancellor extends CardImpl { this.power = new MageInt(1); this.toughness = new MageInt(1); - // Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, you may draw a card. You may draw an additional card if Rayne, Academy Chancellor is enchanted. - this.addAbility(new RayneAcademyChancellorTriggeredAbility()); + // Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, you may draw a card. + // You may draw an additional card if Rayne, Academy Chancellor is enchanted. + Effect drawEffect = new ConditionalOneShotEffect( + new DrawCardSourceControllerEffect(2), + new DrawCardSourceControllerEffect(1), + new EnchantedSourceCondition(), + "you may draw a card. You may draw an additional card if {this} is enchanted" + ); + this.addAbility(new TargetOfOpponentsSpellOrAbilityTriggeredAbility(drawEffect)); } private RayneAcademyChancellor(final RayneAcademyChancellor card) { @@ -47,45 +49,3 @@ public final class RayneAcademyChancellor extends CardImpl { return new RayneAcademyChancellor(this); } } - -class RayneAcademyChancellorTriggeredAbility extends TriggeredAbilityImpl { - - RayneAcademyChancellorTriggeredAbility() { - super(Zone.BATTLEFIELD, new ConditionalOneShotEffect(new DrawCardSourceControllerEffect(2), new DrawCardSourceControllerEffect(1), new EnchantedSourceCondition(), "you may draw a card. You may draw an additional card if {this} is enchanted."), true); - } - - RayneAcademyChancellorTriggeredAbility(final RayneAcademyChancellorTriggeredAbility ability) { - super(ability); - } - - @Override - public RayneAcademyChancellorTriggeredAbility copy() { - return new RayneAcademyChancellorTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.TARGETED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - Player controller = game.getPlayer(this.getControllerId()); - Player targetter = game.getPlayer(event.getPlayerId()); - if (controller != null && targetter != null && !controller.getId().equals(targetter.getId())) { - if (event.getTargetId().equals(controller.getId())) { - return true; - } - Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); - if (permanent != null && this.isControlledBy(permanent.getControllerId())) { - return true; - } - } - return false; - } - - @Override - public String getRule() { - return "Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, you may draw a card. You may draw an additional card if {this} is enchanted."; - } -} diff --git a/Mage.Sets/src/mage/cards/r/ReleaseToTheWind.java b/Mage.Sets/src/mage/cards/r/ReleaseToTheWind.java index 0aa6a88c45..23f102e949 100644 --- a/Mage.Sets/src/mage/cards/r/ReleaseToTheWind.java +++ b/Mage.Sets/src/mage/cards/r/ReleaseToTheWind.java @@ -58,13 +58,12 @@ class ReleaseToTheWindEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Permanent targetPermanent = game.getPermanent(getTargetPointer().getFirst(game, source)); - if (targetPermanent != null) { - return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, targetPermanent, - TargetController.OWNER, Duration.Custom, true, false, true); - } + Permanent targetPermanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (controller == null || targetPermanent == null) { + return false; } - return false; + + return PlayFromNotOwnHandZoneTargetEffect.exileAndPlayFromExile(game, source, targetPermanent, + TargetController.OWNER, Duration.Custom, true, false, true); } } diff --git a/Mage.Sets/src/mage/cards/r/ResourcefulDefense.java b/Mage.Sets/src/mage/cards/r/ResourcefulDefense.java new file mode 100644 index 0000000000..35d104fff0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/ResourcefulDefense.java @@ -0,0 +1,197 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.common.LeavesBattlefieldAllTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.counters.Counter; +import mage.counters.CounterType; +import mage.counters.Counters; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.Target; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledPermanent; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class ResourcefulDefense extends CardImpl { + + public static final FilterControlledPermanent filter2 = new FilterControlledPermanent("another permanent"); + + static { + filter2.add(AnotherPredicate.instance); + } + + public ResourcefulDefense(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}"); + + // Whenever a permanent you control leaves the battlefield, if it had counters on it, put those counters on target permanent you control. + Ability ltbAbility = new ResourcefulDefenseTriggeredAbility(); + ltbAbility.addTarget(new TargetControlledPermanent(filter2)); + this.addAbility(ltbAbility); + + // {4}{W}: Move any number of counters from target permanent you control to another target permanent you control. + Ability ability = new SimpleActivatedAbility(new ResourcefulDefenseMoveCounterEffect(), new ManaCostsImpl<>("{4}{W}")); + + Target fromTarget = new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT); + fromTarget.setTargetTag(1); + + Target toTarget = new TargetPermanent(filter2); + toTarget.setTargetTag(2); + ability.addTarget(fromTarget); + ability.addTarget(toTarget); + + this.addAbility(ability); + } + + private ResourcefulDefense(final ResourcefulDefense card) { + super(card); + } + + @Override + public ResourcefulDefense copy() { + return new ResourcefulDefense(this); + } +} + +class ResourcefulDefenseMoveCounterEffect extends OneShotEffect { + + ResourcefulDefenseMoveCounterEffect() { + super(Outcome.BoostCreature); + this.staticText = "Move any number of counters from target permanent you control to another target permanent you control"; + } + + private ResourcefulDefenseMoveCounterEffect(final ResourcefulDefenseMoveCounterEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent fromPermanent = game.getPermanent(source.getFirstTarget()); + Permanent toPermanent = game.getPermanent(source.getTargets().get(1).getFirstTarget()); + if(controller == null || fromPermanent == null || toPermanent == null) { + return false; + } + + // Counter name and how many to move + Map counterMap = new HashMap<>(); + for (Map.Entry entry : fromPermanent.getCounters(game).entrySet()) { + int num = controller.getAmount( + 0, + entry.getValue().getCount(), + "Choose how many " + entry.getKey() + + " counters to remove from " + fromPermanent.getLogName(), + game); + int newAmount = num + counterMap.getOrDefault(entry.getKey(), 0); + counterMap.put(entry.getKey(), newAmount); + } + + // Move the counters + for (String counterName : counterMap.keySet()) { + toPermanent.addCounters( + CounterType.findByName(counterName).createInstance(counterMap.get(counterName)), + source, + game); + fromPermanent.removeCounters(counterName, counterMap.get(counterName), source, game); + game.informPlayers( + controller.getLogName() + "moved " + + counterMap.get(counterName) + " " + + counterName + "counter" + (counterMap.get(counterName) > 1 ? "s" : "") + + "from " + fromPermanent.getLogName() + + "to " + toPermanent.getLogName() + "." + ); + } + + return true; + } + + @Override + public Effect copy() { + return new ResourcefulDefenseMoveCounterEffect(this); + } +} + +class ResourcefulDefenseTriggeredAbility extends LeavesBattlefieldAllTriggeredAbility { + + ResourcefulDefenseTriggeredAbility() { + super(new ResourcefulDefenseLeaveEffect(), StaticFilters.FILTER_CONTROLLED_PERMANENT); + setTriggerPhrase("Whenever a creature you control leaves the battlefield, if it had counters on it, "); + } + + private ResourcefulDefenseTriggeredAbility(final ResourcefulDefenseTriggeredAbility ability) { + super(ability); + } + + public ResourcefulDefenseTriggeredAbility copy() { + return new ResourcefulDefenseTriggeredAbility(this); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!super.checkTrigger(event, game)) { + return false; + } + + Permanent permanent = ((ZoneChangeEvent) event).getTarget(); + Player controller = game.getPlayer(permanent.getControllerId()); + if (controller == null) { + return false; + } + + Counters counters = permanent.getCounters(game); + if (counters.values().stream().mapToInt(Counter::getCount).noneMatch(x -> x > 0)) { + return false; + } + this.getEffects().setValue("counters", counters); + return true; + } +} + +class ResourcefulDefenseLeaveEffect extends OneShotEffect { + + ResourcefulDefenseLeaveEffect() { + super(Outcome.Benefit); + staticText = "put those counters on target permanent you control"; + } + + private ResourcefulDefenseLeaveEffect(final ResourcefulDefenseLeaveEffect effect) { + super(effect); + } + + @Override + public ResourcefulDefenseLeaveEffect copy() { + return new ResourcefulDefenseLeaveEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + Counters counters = (Counters) this.getValue("counters"); + counters.values() + .stream().filter(counter -> counter.getCount() > 0) + .forEach(counter -> permanent.addCounters(counter, source.getControllerId(), source, game)); + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RiveteersConfluence.java b/Mage.Sets/src/mage/cards/r/RiveteersConfluence.java new file mode 100644 index 0000000000..568ea4b688 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiveteersConfluence.java @@ -0,0 +1,53 @@ +package mage.cards.r; + +import mage.abilities.Mode; +import mage.abilities.effects.common.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class RiveteersConfluence extends CardImpl { + + private static final FilterPermanent damageFilter = new FilterCreatureOrPlaneswalkerPermanent(); + + static { + damageFilter.add(TargetController.NOT_YOU.getControllerPredicate()); + } + + public RiveteersConfluence(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}{R}{G}"); + + // Choose three. You may choose the same mode more than once. + this.getSpellAbility().getModes().setMinModes(3); + this.getSpellAbility().getModes().setMaxModes(3); + this.getSpellAbility().getModes().setEachModeMoreThanOnce(true); + + //• You draw a card and you lose 1 life. + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(1).setText("you draw a card")); + this.getSpellAbility().addEffect(new LoseLifeSourceControllerEffect(1).concatBy("and")); + + //• Riveteers Confluence deals 1 damage to each creature and planeswalker you don’t control. + this.getSpellAbility().addMode(new Mode(new DamageAllEffect(1, damageFilter))); + + //• You may put a land card from your hand or graveyard onto the battlefield tapped. + this.getSpellAbility().addMode(new Mode(new PutCardFromOneOfTwoZonesOntoBattlefieldEffect(StaticFilters.FILTER_CARD_LAND_A, true))); + } + + private RiveteersConfluence(final RiveteersConfluence card) { + super(card); + } + + @Override + public RiveteersConfluence copy() { + return new RiveteersConfluence(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RobTheArchives.java b/Mage.Sets/src/mage/cards/r/RobTheArchives.java index ca4541f5b4..319df81848 100644 --- a/Mage.Sets/src/mage/cards/r/RobTheArchives.java +++ b/Mage.Sets/src/mage/cards/r/RobTheArchives.java @@ -17,7 +17,7 @@ public final class RobTheArchives extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{R}"); // Casualty 1 - this.addAbility(new CasualtyAbility(this, 1)); + this.addAbility(new CasualtyAbility(1)); // Exile the top two cards of your library. You may play those cards this turn. this.getSpellAbility().addEffect(new ExileTopXMayPlayUntilEndOfTurnEffect(2, false)); diff --git a/Mage.Sets/src/mage/cards/r/RooftopNuisance.java b/Mage.Sets/src/mage/cards/r/RooftopNuisance.java index 5279d5a34b..99aafa242d 100644 --- a/Mage.Sets/src/mage/cards/r/RooftopNuisance.java +++ b/Mage.Sets/src/mage/cards/r/RooftopNuisance.java @@ -20,7 +20,7 @@ public final class RooftopNuisance extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{U}"); // Casualty 1 - this.addAbility(new CasualtyAbility(this, 1)); + this.addAbility(new CasualtyAbility(1)); // Tap target creature. It doesn't untap during its controller's next untap step. this.getSpellAbility().addEffect(new TapTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/s/ShareTheSpoils.java b/Mage.Sets/src/mage/cards/s/ShareTheSpoils.java index a290181a8e..b6dc06542b 100644 --- a/Mage.Sets/src/mage/cards/s/ShareTheSpoils.java +++ b/Mage.Sets/src/mage/cards/s/ShareTheSpoils.java @@ -27,8 +27,8 @@ import java.util.UUID; */ public final class ShareTheSpoils extends CardImpl { - public ShareTheSpoils(UUID ownderId, CardSetInfo setInfo) { - super(ownderId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}"); + public ShareTheSpoils(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}"); // When Share the Spoils enters the battlefield or an opponent loses the game, // exile the top card of each player’s library.exile the top card of each player’s library. diff --git a/Mage.Sets/src/mage/cards/s/ShieldBroker.java b/Mage.Sets/src/mage/cards/s/ShieldBroker.java new file mode 100644 index 0000000000..5d2b4e380e --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ShieldBroker.java @@ -0,0 +1,63 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.TargetHasCounterCondition; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.continuous.GainControlTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.counters.CounterType; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.CommanderPredicate; +import mage.game.Game; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class ShieldBroker extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("noncommander creature you don't control"); + static { + filter.add(Predicates.not(CommanderPredicate.instance)); + filter.add(TargetController.NOT_YOU.getControllerPredicate()); + } + + public ShieldBroker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}{U}"); + + this.addSubType(SubType.CEPHALID, SubType.ADVISOR); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // When Shield Broker enters the battlefield, put a shield counter on target noncommander creature you don’t control. + // You gain control of that creature for as long as it has a shield counter on it. + // (If it would be dealt damage or destroyed, remove a shield counter from it instead.) + Ability etbAbility = new EntersBattlefieldTriggeredAbility(new AddCountersTargetEffect(CounterType.SHIELD.createInstance())); + Effect gainControlEffect = new GainControlTargetEffect(Duration.Custom, false, null, new TargetHasCounterCondition(CounterType.SHIELD)); + gainControlEffect.setText("You gain control of that creature for as long as it has a shield counter on it. " + + "(If it would be dealt damage or destroyed, remove a shield counter from it instead.)"); + etbAbility.addEffect(gainControlEffect); + etbAbility.addTarget(new TargetCreaturePermanent(filter)); + this.addAbility(etbAbility); + } + + private ShieldBroker(final ShieldBroker card) { + super(card); + } + + @Override + public ShieldBroker copy() { + return new ShieldBroker(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SinisterConcierge.java b/Mage.Sets/src/mage/cards/s/SinisterConcierge.java new file mode 100644 index 0000000000..db9dcb4b95 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SinisterConcierge.java @@ -0,0 +1,126 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.costs.common.ExileSourceFromGraveCost; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.continuous.GainSuspendEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.SuspendAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCreaturePermanent; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class SinisterConcierge extends CardImpl { + + public SinisterConcierge(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.addSubType(SubType.HUMAN, SubType.WIZARD); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // When Sinister Concierge dies, you may exile it and put three time counters on it. + // If you do, exile up to one target creature and put three time counters on it. + // Each card exiled this way that doesn't have suspend gains suspend. + // (For each card with suspend, its owner removes a time counter from it at the beginning of their upkeep. + // When the last is removed, they cast it without paying its mana cost. Those creature spells have haste.) + Ability ability = new DiesSourceTriggeredAbility( + new DoIfCostPaid( + new SinisterConciergeEffect(), + new ExileSourceFromGraveCost() + ).setText("you may exile it and put three time counters on it") + ); + ability.addTarget(new TargetCreaturePermanent(0, 1)); + this.addAbility(ability); + } + + private SinisterConcierge(final SinisterConcierge card) { + super(card); + } + + @Override + public SinisterConcierge copy() { + return new SinisterConcierge(this); + } +} + +class SinisterConciergeEffect extends OneShotEffect { + public SinisterConciergeEffect() { + super(Outcome.Removal); + this.staticText = "you may exile it and put three time counters on it. " + + "If you do, exile up to one target creature and put three time counters on it. " + + "Each card exiled this way that doesn't have suspend gains suspend. " + + "(For each card with suspend, its owner removes a time counter from it at the beginning of their upkeep. " + + "When the last is removed, they cast it without paying its mana cost. Those creature spells have haste.)"; + } + + private SinisterConciergeEffect(final SinisterConciergeEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Card card = game.getCard(source.getSourceId()); + Permanent targetCreature = game.getPermanent(this.getTargetPointer().getFirst(game, source)); + if (controller == null || card == null || targetCreature == null) { + return false; + } + + // Put the time counters on the Sinister Concierge and give it Suspend + if (game.getState().getZone(card.getId()) == Zone.EXILED) { + Effect addCountersSourceEffect = new AddCountersSourceEffect(CounterType.TIME.createInstance(), StaticValue.get(3), false ,true); + boolean sourceCardShouldGetSuspend = addCountersSourceEffect.apply(game, source); + + if (sourceCardShouldGetSuspend && !card.getAbilities(game).containsClass(SuspendAbility.class)) { + game.addEffect(new GainSuspendEffect(new MageObjectReference(card, game)), source); + } + } + + // Exile, put time counters, and give suspend for target + Effect exileTarget = new ExileTargetEffect(); + exileTarget.setTargetPointer(this.getTargetPointer()); + if (exileTarget.apply(game, source)) { + Effect addCountersTargetEffect = new AddCountersTargetEffect(CounterType.TIME.createInstance(3)); + addCountersTargetEffect.setTargetPointer(this.getTargetPointer()); + boolean targetCardShouldGetSuspend = addCountersTargetEffect.apply(game, source); + + if (targetCardShouldGetSuspend && !targetCreature.getAbilities(game).containsClass(SuspendAbility.class)) { + Card targetCard = game.getCard(getTargetPointer().getFirst(game, source)); + if (!targetCard.getAbilities(game).containsClass(SuspendAbility.class)) { + game.addEffect(new GainSuspendEffect(new MageObjectReference(targetCard, game)), source); + } + } + } + + return true; + } + + @Override + public SinisterConciergeEffect copy() { + return new SinisterConciergeEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SkywayRobber.java b/Mage.Sets/src/mage/cards/s/SkywayRobber.java new file mode 100644 index 0000000000..0c1833339c --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SkywayRobber.java @@ -0,0 +1,127 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.EscapesWithAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.keyword.EscapeAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; +import mage.game.ExileZone; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInExile; +import mage.util.CardUtil; + +import java.util.Set; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class SkywayRobber extends CardImpl { + + public SkywayRobber(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}"); + + this.addSubType(SubType.BIRD, SubType.ROGUE); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Escape—{3}{U}, Exile five other cards from your graveyard. (You may cast this card from your graveyard for its escape cost.) + this.addAbility(new EscapeAbility(this, "{3}{U}", 5)); + + // Skyway Robber escapes with “Whenever Skyway Robber deals combat damage to a player, you may cast an artifact, instant, or sorcery spell from among cards exiled with Skyway Robber without paying its mana cost.” + // NOTE: Optional is handled by the effect, this way it won't prompt the player if no valid cards are available + TriggeredAbility dealsDamageAbility = new DealsCombatDamageToAPlayerTriggeredAbility(new SkywayRobberCastForFreeEffect(), false); + this.addAbility(new EscapesWithAbility(0, dealsDamageAbility)); + } + + private SkywayRobber(final SkywayRobber card) { + super(card); + } + + @Override + public SkywayRobber copy() { + return new SkywayRobber(this); + } +} + +class SkywayRobberCastForFreeEffect extends OneShotEffect { + + private static final FilterCard filter = new FilterCard("an artifact, instant, or sorcery card"); + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.INSTANT.getPredicate(), + CardType.SORCERY.getPredicate()) + ); + } + + public SkywayRobberCastForFreeEffect() { + super(Outcome.PlayForFree); + this.staticText = "you may cast an artifact, instant, or sorcery spell from among cards exiled with Skyway Robber without paying its mana cost"; + } + + private SkywayRobberCastForFreeEffect(final SkywayRobberCastForFreeEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + String exileZoneName = CardUtil.getObjectZoneString(CardUtil.SOURCE_EXILE_ZONE_TEXT, source.getSourceId(), game, source.getSourceObjectZoneChangeCounter()-1, false); + UUID exileId = CardUtil.getExileZoneId(exileZoneName, game); + ExileZone exileZone = game.getExile().getExileZone(exileId); + if (exileZone == null) { + return false; + } + + Set possibleCards = exileZone.getCards(filter, game); + if (possibleCards.isEmpty()) { + return false; + } + + boolean choseToPlay = controller.chooseUse( + Outcome.PlayForFree, + "Cast an artifact, instant, or sorcery spell from among cards exiled with Skyway Robber without paying its mana cost?", + source, + game); + if (!choseToPlay) { + return false; + } + + TargetCardInExile target = new TargetCardInExile(filter, exileId); + if (!controller.chooseTarget(Outcome.PlayForFree, target, source, game)) { + return false; + } + + Card chosenCard = game.getCard(target.getFirstTarget()); + if (chosenCard == null) { + return false; + } + + return CardUtil.castSpellWithAttributesForFree(controller, source, game, chosenCard); + } + + @Override + public SkywayRobberCastForFreeEffect copy() { + return new SkywayRobberCastForFreeEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SmugglersBuggy.java b/Mage.Sets/src/mage/cards/s/SmugglersBuggy.java new file mode 100644 index 0000000000..4b202ad188 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SmugglersBuggy.java @@ -0,0 +1,91 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.HideawayPlayEffect; +import mage.abilities.effects.common.ReturnToHandSourceEffect; +import mage.abilities.keyword.CrewAbility; +import mage.abilities.keyword.HideawayAbility; +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.permanent.Permanent; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class SmugglersBuggy extends CardImpl { + + public SmugglersBuggy(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}"); + + this.addSubType(SubType.VEHICLE); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // Hideaway 4 + // (When this artifact enters the battlefield, look at the top four cards of your library, + // exile one face down, then put the rest on the bottom in a random order.) + this.addAbility(new HideawayAbility(4)); + + // Whenever Smuggler’s Buggy deals combat damage to a player, you may cast the exiled card without paying its mana cost. + // If you do, return Smuggler’s Buggy to its owner’s hand. + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(new SmugglersBuggyCastAndReturnEffect(), true)); + + // Crew 2 + this.addAbility(new CrewAbility(2)); + } + + private SmugglersBuggy(final SmugglersBuggy card) { + super(card); + } + + @Override + public SmugglersBuggy copy() { + return new SmugglersBuggy(this); + } +} + +class SmugglersBuggyCastAndReturnEffect extends OneShotEffect { + + SmugglersBuggyCastAndReturnEffect() { + super(Outcome.Benefit); + this.staticText = "you may cast the exiled card without paying its mana cost. " + + "If you do, return Smuggler's Buggy to its owner's hand"; + } + + private SmugglersBuggyCastAndReturnEffect(final SmugglersBuggyCastAndReturnEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent smugglersBuggy = game.getPermanent(source.getSourceId()); + if (controller == null || smugglersBuggy == null) { + return false; + } + + Effect hideawayPlayEffect = new HideawayPlayEffect(); + if (!hideawayPlayEffect.apply(game, source)) { + return false; + } + + Effect returnToHandEffect = new ReturnToHandSourceEffect(); + return returnToHandEffect.apply(game, source); + } + + @Override + public Effect copy() { + return new SmugglersBuggyCastAndReturnEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SoulsFire.java b/Mage.Sets/src/mage/cards/s/SoulsFire.java index bc1d5029cb..5868d5ae0f 100644 --- a/Mage.Sets/src/mage/cards/s/SoulsFire.java +++ b/Mage.Sets/src/mage/cards/s/SoulsFire.java @@ -1,15 +1,9 @@ package mage.cards.s; -import mage.abilities.Ability; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DamageWithPowerFromOneToAnotherTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.players.Player; import mage.target.common.TargetAnyTarget; import mage.target.common.TargetControlledCreaturePermanent; @@ -23,9 +17,8 @@ public final class SoulsFire extends CardImpl { public SoulsFire(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{R}"); - // Target creature you control on the battlefield deals damage equal to its power to any target. - this.getSpellAbility().addEffect(new SoulsFireEffect()); + this.getSpellAbility().addEffect(new DamageWithPowerFromOneToAnotherTargetEffect()); this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); this.getSpellAbility().addTarget(new TargetAnyTarget()); } @@ -39,48 +32,3 @@ public final class SoulsFire extends CardImpl { return new SoulsFire(this); } } - -class SoulsFireEffect extends OneShotEffect { - - public SoulsFireEffect() { - super(Outcome.Damage); - this.staticText = "Target creature you control deals damage equal to its power to any target"; - } - - public SoulsFireEffect(final SoulsFireEffect effect) { - super(effect); - } - - @Override - public SoulsFireEffect copy() { - return new SoulsFireEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Permanent sourcePermanent = game.getPermanent(source.getFirstTarget()); - if (sourcePermanent == null) { - sourcePermanent = (Permanent) game.getLastKnownInformation(source.getSourceId(), Zone.BATTLEFIELD); - } - if (sourcePermanent == null) { - return false; - } - - UUID targetId = source.getTargets().get(1).getFirstTarget(); - int damage = sourcePermanent.getPower().getValue(); - - Permanent permanent = game.getPermanent(targetId); - if (permanent != null) { - permanent.damage(damage, sourcePermanent.getId(), source, game, false, true); - return true; - } - - Player player = game.getPlayer(targetId); - if (player != null) { - player.damage(damage, sourcePermanent.getId(), source, game); - return true; - } - - return false; - } -} diff --git a/Mage.Sets/src/mage/cards/s/SwiftWarkite.java b/Mage.Sets/src/mage/cards/s/SwiftWarkite.java index a57c3f6088..8ab607182a 100644 --- a/Mage.Sets/src/mage/cards/s/SwiftWarkite.java +++ b/Mage.Sets/src/mage/cards/s/SwiftWarkite.java @@ -10,11 +10,11 @@ import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbil import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.PutCardFromOneOfTwoZonesOntoBattlefieldEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.HasteAbility; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -22,15 +22,10 @@ import mage.constants.SubType; import mage.constants.ComparisonType; import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.predicate.mageobject.ManaValuePredicate; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.Player; -import mage.target.Target; -import mage.target.common.TargetCardInHand; -import mage.target.common.TargetCardInYourGraveyard; import mage.target.targetpointer.FixedTarget; /** @@ -39,6 +34,13 @@ import mage.target.targetpointer.FixedTarget; */ public final class SwiftWarkite extends CardImpl { + private static final FilterCard filter = new FilterCard("creature card with mana value 3 or less from your hand or graveyard"); + + static { + filter.add(CardType.CREATURE.getPredicate()); + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 4)); + } + public SwiftWarkite(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{4}{B}{R}"); this.subtype.add(SubType.DRAGON); @@ -49,8 +51,10 @@ public final class SwiftWarkite extends CardImpl { this.addAbility(FlyingAbility.getInstance()); // When Swift Warkite enters the battlefield, you may put a creature card with converted mana cost 3 or less from your hand or graveyard onto the battlefield. That creature gains haste. Return it to your hand at the beginning of the next end step. - this.addAbility(new EntersBattlefieldTriggeredAbility(new SwiftWarkiteEffect(), true)); - + this.addAbility(new EntersBattlefieldTriggeredAbility( + new PutCardFromOneOfTwoZonesOntoBattlefieldEffect(filter, false, new SwiftWarkiteEffect()), + true) + ); } private SwiftWarkite(final SwiftWarkite card) { @@ -65,16 +69,9 @@ public final class SwiftWarkite extends CardImpl { class SwiftWarkiteEffect extends OneShotEffect { - private static final FilterCard filter = new FilterCard("creature card with mana value 3 or less from your hand or graveyard"); - - static { - filter.add(CardType.CREATURE.getPredicate()); - filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 4)); - } - SwiftWarkiteEffect() { - super(Outcome.PutCardInPlay); - this.staticText = "you may put a creature card with mana value 3 or less from your hand or graveyard onto the battlefield. That creature gains haste. Return it to your hand at the beginning of the next end step"; + super(Outcome.AddAbility); + this.staticText = "that creature gains haste. Return it to your hand at the beginning of the next end step"; } SwiftWarkiteEffect(final SwiftWarkiteEffect effect) { @@ -88,46 +85,19 @@ class SwiftWarkiteEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - if (controller.chooseUse(Outcome.PutCardInPlay, "Put a creature card from your hand? (No = from your graveyard)", source, game)) { - Target target = new TargetCardInHand(0, 1, filter); - controller.choose(outcome, target, source, game); - Card card = controller.getHand().get(target.getFirstTarget(), game); - if (card != null) { - if (controller.moveCards(card, Zone.BATTLEFIELD, source, game)) { - Permanent creature = game.getPermanent(card.getId()); - if (creature != null) { - ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom); - effect.setTargetPointer(new FixedTarget(creature.getId(), creature.getZoneChangeCounter(game))); - game.addEffect(effect, source); - Effect effect2 = new ReturnToHandTargetEffect(); - effect2.setTargetPointer(new FixedTarget(creature.getId(), creature.getZoneChangeCounter(game))); - DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect2); - game.addDelayedTriggeredAbility(delayedAbility, source); - } - } - } - } else { - Target target = new TargetCardInYourGraveyard(0, 1, filter); - target.choose(Outcome.PutCardInPlay, source.getControllerId(), source.getSourceId(), source, game); - Card card = controller.getGraveyard().get(target.getFirstTarget(), game); - if (card != null) { - controller.moveCards(card, Zone.BATTLEFIELD, source, game); - Permanent creature = game.getPermanent(card.getId()); - if (creature != null) { - ContinuousEffect effect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom); - effect.setTargetPointer(new FixedTarget(creature.getId(), creature.getZoneChangeCounter(game))); - game.addEffect(effect, source); - Effect effect2 = new ReturnToHandTargetEffect(); - effect2.setTargetPointer(new FixedTarget(creature.getId(), creature.getZoneChangeCounter(game))); - DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect2); - game.addDelayedTriggeredAbility(delayedAbility, source); - } - } - } - return true; + Permanent movedCreature = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (movedCreature == null) { + return false; } - return false; + + ContinuousEffect gainHasteEffect = new GainAbilityTargetEffect(HasteAbility.getInstance(), Duration.Custom); + gainHasteEffect.setTargetPointer(new FixedTarget(movedCreature.getId(), movedCreature.getZoneChangeCounter(game))); + game.addEffect(gainHasteEffect, source); + + Effect returnToHandEffect = new ReturnToHandTargetEffect(); + returnToHandEffect.setTargetPointer(new FixedTarget(movedCreature.getId(), movedCreature.getZoneChangeCounter(game))); + DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(returnToHandEffect); + game.addDelayedTriggeredAbility(delayedAbility, source); + return true; } } diff --git a/Mage.Sets/src/mage/cards/s/SwindlersScheme.java b/Mage.Sets/src/mage/cards/s/SwindlersScheme.java new file mode 100644 index 0000000000..2f991f8748 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SwindlersScheme.java @@ -0,0 +1,136 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CounterTargetEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.players.Library; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class SwindlersScheme extends CardImpl { + + public SwindlersScheme(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}"); + + // Whenever an opponent casts a spell from their hand, you may reveal the top card of your library. + // If it shares a card type with that spell, counter that spell and that opponent may cast the revealed card without paying its mana cost. + this.addAbility(new SwindlersSchemeOpponentCastTriggeredAbility()); + } + + private SwindlersScheme(final SwindlersScheme card) { + super(card); + } + + @Override + public SwindlersScheme copy() { + return new SwindlersScheme(this); + } +} + +/** + * TODO: Creating a custom ability since SpellCastOpponentTriggeredAbility is getting out of hand and needs + * to be refactored to not use telescoping constructors. + */ +class SwindlersSchemeOpponentCastTriggeredAbility extends TriggeredAbilityImpl { + + SwindlersSchemeOpponentCastTriggeredAbility() { + super(Zone.BATTLEFIELD, new SwindlersSchemeEffect(), true); + setTriggerPhrase("Whenever an opponent casts a spell from their hand, "); + } + + private SwindlersSchemeOpponentCastTriggeredAbility(final SwindlersSchemeOpponentCastTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Player controller = game.getPlayer(this.getControllerId()); + if (controller == null || !controller.hasOpponent(event.getPlayerId(), game)) { + return false; + } + + Spell spell = game.getStack().getSpell(event.getTargetId()); + if (spell == null || spell.getFromZone() != Zone.HAND) { + return false; + } + getEffects().setValue("spellCast", spell); + return true; + } + + @Override + public SwindlersSchemeOpponentCastTriggeredAbility copy() { + return new SwindlersSchemeOpponentCastTriggeredAbility(this); + } +} + +class SwindlersSchemeEffect extends OneShotEffect { + + SwindlersSchemeEffect() { + super(Outcome.Detriment); + this.staticText = "reveal the top card of your library. " + + "If it shares a card type with that spell, counter that spell and that opponent may cast the revealed card without paying its mana cost."; + } + + private SwindlersSchemeEffect(final SwindlersSchemeEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Spell spell = (Spell) getValue("spellCast"); + Library library = controller.getLibrary(); + + Card cardFromTop = library.getFromTop(game); + if (cardFromTop == null) { + return false; + } + + if (cardFromTop.getCardType(game).stream().noneMatch(spell.getCardType(game)::contains)) { + return false; + } + Player opponent = game.getPlayer(spell.getControllerId()); + + Effect counterEffect = new CounterTargetEffect(); + counterEffect.setTargetPointer(new FixedTarget(spell.getId())); + counterEffect.apply(game, source); + + CardUtil.castSpellWithAttributesForFree(opponent, source, game, cardFromTop); + + return true; + } + + @Override + public SwindlersSchemeEffect copy() { + return new SwindlersSchemeEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SyrixCarrierOfTheFlame.java b/Mage.Sets/src/mage/cards/s/SyrixCarrierOfTheFlame.java new file mode 100644 index 0000000000..e5430c7253 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SyrixCarrierOfTheFlame.java @@ -0,0 +1,188 @@ +package mage.cards.s; + +import mage.ApprovingObject; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DamageWithPowerFromOneToAnotherTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.HasteAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetAnyTarget; +import mage.watchers.Watcher; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class SyrixCarrierOfTheFlame extends CardImpl { + + private static final String description = "Phoenix you control"; + private static final FilterPermanent anotherPhoenixFilter = new FilterControlledPermanent("another Phoenix you control"); + private static final FilterPermanent phoenixFilter = new FilterControlledPermanent(description); + static { + anotherPhoenixFilter.add(AnotherPredicate.instance); + anotherPhoenixFilter.add(SubType.PHOENIX.getPredicate()); + phoenixFilter.add(SubType.PHOENIX.getPredicate()); + } + + public SyrixCarrierOfTheFlame(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{R}"); + + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.PHOENIX); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying, haste + this.addAbility(FlyingAbility.getInstance()); + this.addAbility(HasteAbility.getInstance()); + + // At the beginning of each end step, if a creature card left your graveyard this turn, + // target Phoenix you control deals damage equal to its power to any target. + BeginningOfEndStepTriggeredAbility ability = new BeginningOfEndStepTriggeredAbility( + new DamageWithPowerFromOneToAnotherTargetEffect(), + TargetController.EACH_PLAYER, + SyrixCarrierOfTheFlameCondition.instance, + false + ); + ability.addTarget(new TargetPermanent(phoenixFilter)); + ability.addTarget(new TargetAnyTarget()); + ability.addWatcher(new SyrixCarrierOfTheFlameWatcher()); + this.addAbility(ability); + + // Whenever another Phoenix you control dies, you may cast Syrix, Carrier of the Flame from your graveyard. + this.addAbility(new DiesCreatureTriggeredAbility( + Zone.GRAVEYARD, + new SyrixCarrierOfTheFlameCastEffect(), + true, + anotherPhoenixFilter, + false) + ); + } + + private SyrixCarrierOfTheFlame(final SyrixCarrierOfTheFlame card) { + super(card); + } + + @Override + public SyrixCarrierOfTheFlame copy() { + return new SyrixCarrierOfTheFlame(this); + } +} + +/** + * Based on Harness the Storm + */ +class SyrixCarrierOfTheFlameCastEffect extends OneShotEffect { + SyrixCarrierOfTheFlameCastEffect() { + super(Outcome.Benefit); + this.staticText = "you may cast {this} from your graveyard"; + } + + SyrixCarrierOfTheFlameCastEffect(final SyrixCarrierOfTheFlameCastEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + + Card card = game.getCard(source.getSourceId()); + if (card == null) { + return false; + } + if (controller.chooseUse(Outcome.Benefit, "Cast " + card.getIdName() + " from your graveyard?", source, game)) { + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + controller.cast(controller.chooseAbilityForCast(card, game, false), + game, false, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + } + return true; + } + + @Override + public SyrixCarrierOfTheFlameCastEffect copy() { + return new SyrixCarrierOfTheFlameCastEffect(this); + } +} + +/** + * Creature card left your graveyard this turn + */ +enum SyrixCarrierOfTheFlameCondition implements Condition { + instance; + + private static final String string = "a creature card left your graveyard this turn"; + + @Override + public boolean apply(Game game, Ability source) { + SyrixCarrierOfTheFlameWatcher watcher = game.getState().getWatcher(SyrixCarrierOfTheFlameWatcher.class); + return watcher != null && watcher.hadACreatureLeave(source.getControllerId()); + } + + @Override + public String toString() { + return string; + } +} + +/** + * Creature card left your graveyard this turn + */ +class SyrixCarrierOfTheFlameWatcher extends Watcher { + + // Player IDs who had a creature card leave their graveyard + private final Set creatureCardLeftPlayerIds = new HashSet<>(); + + SyrixCarrierOfTheFlameWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (!(event.getType() == GameEvent.EventType.ZONE_CHANGE && event instanceof ZoneChangeEvent)) { + return; + } + ZoneChangeEvent zoneChangeEvent = (ZoneChangeEvent) event; + + if (zoneChangeEvent.getFromZone() != Zone.GRAVEYARD) { + return; + } + + Card card = zoneChangeEvent.getTarget(); + if (card != null && card.isCreature(game)) { + creatureCardLeftPlayerIds.add(card.getOwnerId()); + } + } + + public boolean hadACreatureLeave(UUID playerId) { + return creatureCardLeftPlayerIds.contains(playerId); + } + + @Override + public void reset() { + super.reset(); + creatureCardLeftPlayerIds.clear(); + } +} diff --git a/Mage.Sets/src/mage/cards/t/TenuousTruce.java b/Mage.Sets/src/mage/cards/t/TenuousTruce.java new file mode 100644 index 0000000000..39d17718d4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TenuousTruce.java @@ -0,0 +1,122 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.BeginningOfEndStepTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.*; +import mage.abilities.keyword.EnchantAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPlayer; +import mage.target.common.TargetOpponent; + +import java.util.Set; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class TenuousTruce extends CardImpl { + + public TenuousTruce(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{W}"); + this.addSubType(SubType.AURA); + + // Enchant opponent + TargetPlayer targetOpponent = new TargetOpponent(); + this.getSpellAbility().addTarget(targetOpponent); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.DrawCard)); + this.addAbility(new EnchantAbility(targetOpponent.getTargetName())); + + // At the beginning of enchanted opponent’s end step, you and that player each draw a card. + Ability drawAbility = new BeginningOfEndStepTriggeredAbility( + new DrawCardSourceControllerEffect(1).setText("you "), + TargetController.ENCHANTED, + false); + Effect enchantedPlayerDrawEffect = new DrawCardTargetEffect(1); + enchantedPlayerDrawEffect.concatBy("and").setText("that player each draw a card"); + drawAbility.addEffect(enchantedPlayerDrawEffect); + this.addAbility(drawAbility); + + // When you attack enchanted opponent or a planeswalker they control + // or when they attack you or a planeswalker you control, + // sacrifice Tenuous Truce. + this.addAbility(new TenuousTruceAttackTriggeredAbility()); + } + + private TenuousTruce(final TenuousTruce card) { + super(card); + } + + @Override + public TenuousTruce copy() { + return new TenuousTruce(this); + } +} + +class TenuousTruceAttackTriggeredAbility extends TriggeredAbilityImpl { + + TenuousTruceAttackTriggeredAbility() { + super(Zone.BATTLEFIELD, new SacrificeSourceEffect(), false); + setTriggerPhrase("When you attack enchanted opponent or a planeswalker they control " + + "or when they attack you or a planeswalker you control, "); + } + + TenuousTruceAttackTriggeredAbility(final TenuousTruceAttackTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DECLARED_ATTACKERS; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Permanent tenuousTruce = game.getPermanent(this.getSourceId()); + Player controller = game.getPlayer(this.getControllerId()); + Player attacker = game.getPlayer(game.getCombat().getAttackingPlayerId()); + if (tenuousTruce == null || controller == null || attacker == null) { + return false; + } + + Player enchantedPlayer = game.getPlayer(tenuousTruce.getAttachedTo()); + if (enchantedPlayer == null) { + return false; + } + + Set defenderIds = game.getCombat().getDefenders(); + if (controller.equals(attacker)) { + return TenuousTruceAttackTriggeredAbility.playerOneAttackingPlayerBOrTheirPlaneswalker(controller.getId(), enchantedPlayer.getId(), defenderIds, game); + } else if (enchantedPlayer.equals(attacker)) { + return TenuousTruceAttackTriggeredAbility.playerOneAttackingPlayerBOrTheirPlaneswalker(enchantedPlayer.getId(), controller.getId(), defenderIds, game); + } else { + return false; + } + } + + private static boolean playerOneAttackingPlayerBOrTheirPlaneswalker(UUID playerAId, UUID playerBId, Set defenderIds, Game game) { + if (defenderIds.contains(playerBId)) { + return true; + } + // Check planeswalkers + for (UUID defenderId : defenderIds) { + Permanent perm = game.getPermanent(defenderId); + if (perm != null && perm.getOwnerId().equals(playerBId)) { + return true; + } + } + return false; + } + + @Override + public TenuousTruceAttackTriggeredAbility copy() { + return new TenuousTruceAttackTriggeredAbility(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/t/TheOzolith.java b/Mage.Sets/src/mage/cards/t/TheOzolith.java index 5fc3fbf55a..773bc465cd 100644 --- a/Mage.Sets/src/mage/cards/t/TheOzolith.java +++ b/Mage.Sets/src/mage/cards/t/TheOzolith.java @@ -4,6 +4,7 @@ import mage.abilities.Ability; import mage.abilities.common.BeginningOfCombatTriggeredAbility; import mage.abilities.common.LeavesBattlefieldAllTriggeredAbility; import mage.abilities.condition.Condition; +import mage.abilities.condition.common.SourceHasCountersCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; @@ -40,7 +41,7 @@ public final class TheOzolith extends CardImpl { Ability ability = new ConditionalInterveningIfTriggeredAbility( new BeginningOfCombatTriggeredAbility( new TheOzolithMoveCountersEffect(), TargetController.YOU, true - ), TheOzolithCondition.instance, "At the beginning of combat on your turn, " + + ), SourceHasCountersCondition.instance, "At the beginning of combat on your turn, " + "if {this} has counters on it, you may move all counters from {this} onto target creature." ); ability.addTarget(new TargetCreaturePermanent()); @@ -118,32 +119,11 @@ class TheOzolithLeaveEffect extends OneShotEffect { return false; } counters.values() - .stream() .forEach(counter -> permanent.addCounters(counter, source.getControllerId(), source, game)); return true; } } -enum TheOzolithCondition implements Condition { - instance; - - @Override - public boolean apply(Game game, Ability source) { - Permanent permanent = game.getPermanent(source.getSourceId()); - if (permanent == null) { - return false; - } - return permanent != null - && permanent - .getCounters(game) - .values() - .stream() - .mapToInt(Counter::getCount) - .max() - .orElse(0) > 0; - } -} - class TheOzolithMoveCountersEffect extends OneShotEffect { TheOzolithMoveCountersEffect() { diff --git a/Mage.Sets/src/mage/cards/t/ThreefoldSignal.java b/Mage.Sets/src/mage/cards/t/ThreefoldSignal.java new file mode 100644 index 0000000000..0df104f144 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThreefoldSignal.java @@ -0,0 +1,81 @@ +package mage.cards.t; + +import mage.MageObject; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.continuous.EachSpellYouCastHasReplicateEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.ModalDoubleFacesCardHalf; +import mage.cards.SplitCardHalf; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.filter.FilterSpell; +import mage.filter.predicate.Predicate; +import mage.game.Game; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class ThreefoldSignal extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("spell you cast that's exactly three colors"); + static { + filter.add(ThreeColorPredicate.instance); + } + + public ThreefoldSignal(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); + + // When Threefold Signal enters the battlefield, scry 3. + this.addAbility(new EntersBattlefieldTriggeredAbility(new ScryEffect(3))); + + // Each spell you cast that’s exactly three colors has replicate {3}. + // (When you cast it, copy it for each time you paid its replicate cost. + // You may choose new targets for the copies. + // A copy of a permanent spell becomes a token.) + this.addAbility(new SimpleStaticAbility(new EachSpellYouCastHasReplicateEffect(filter, new GenericManaCost(3)))); + } + + private ThreefoldSignal(final ThreefoldSignal card) { + super(card); + } + + @Override + public ThreefoldSignal copy() { + return new ThreefoldSignal(this); + } +} + +/** + * Based on MultiColorPredicate + */ +enum ThreeColorPredicate implements Predicate { + instance; + + @Override + public boolean apply(MageObject input, Game game) { + // 708.3. Each split card that consists of two halves with different colored mana symbols in their mana costs + // is a multicolored card while it's not a spell on the stack. While it's a spell on the stack, it's only the + // color or colors of the half or halves being cast. # + if (input instanceof SplitCardHalf + && game.getState().getZone(input.getId()) != Zone.STACK) { + return 3 == ((SplitCardHalf) input).getMainCard().getColor(game).getColorCount(); + } else if (input instanceof ModalDoubleFacesCardHalf + && (game.getState().getZone(input.getId()) != Zone.STACK && game.getState().getZone(input.getId()) != Zone.BATTLEFIELD)) { + // While a double-faced card isn’t on the stack or battlefield, consider only the characteristics of its front face. + return 3 == ((ModalDoubleFacesCardHalf) input).getMainCard().getColor(game).getColorCount(); + } else { + return 3 == input.getColor(game).getColorCount(); + } + } + + @Override + public String toString() { + return "Multicolored"; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/t/TurfWar.java b/Mage.Sets/src/mage/cards/t/TurfWar.java index 2188bcfa89..1f59becfa2 100644 --- a/Mage.Sets/src/mage/cards/t/TurfWar.java +++ b/Mage.Sets/src/mage/cards/t/TurfWar.java @@ -114,6 +114,7 @@ class TurfWarTriggeredAbility extends TriggeredAbilityImpl { public TurfWarTriggeredAbility() { super(Zone.BATTLEFIELD, new TurfWarControlEffect()); + setTriggerPhrase("Whenever a creature deals combat damage to a player, if that player controls one or more lands with contested counters on them, "); } private TurfWarTriggeredAbility(final TurfWarTriggeredAbility ability) { @@ -155,11 +156,6 @@ class TurfWarTriggeredAbility extends TriggeredAbilityImpl { } return false; } - - @Override - public String getTriggerPhrase() { - return "Whenever a creature deals combat damage to a player, if that player controls one or more lands with contested counters on them, "; - } } class TurfWarControlEffect extends OneShotEffect { diff --git a/Mage.Sets/src/mage/cards/u/UnsettledMariner.java b/Mage.Sets/src/mage/cards/u/UnsettledMariner.java index 9607ac4209..36097f1a93 100644 --- a/Mage.Sets/src/mage/cards/u/UnsettledMariner.java +++ b/Mage.Sets/src/mage/cards/u/UnsettledMariner.java @@ -2,6 +2,7 @@ package mage.cards.u; import mage.MageInt; import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.TargetOfOpponentsSpellOrAbilityTriggeredAbility; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.effects.Effect; import mage.abilities.effects.common.CounterUnlessPaysEffect; @@ -33,8 +34,9 @@ public final class UnsettledMariner extends CardImpl { // Changeling this.addAbility(new ChangelingAbility()); - // Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, counter that spell or ability unless its controller pays {1}. - this.addAbility(new UnsettledMarinerTriggeredAbility()); + // Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, + // counter that spell or ability unless its controller pays {1}. + this.addAbility(new TargetOfOpponentsSpellOrAbilityTriggeredAbility(new CounterUnlessPaysEffect(new GenericManaCost(1)))); } private UnsettledMariner(final UnsettledMariner card) { @@ -46,47 +48,3 @@ public final class UnsettledMariner extends CardImpl { return new UnsettledMariner(this); } } - -class UnsettledMarinerTriggeredAbility extends TriggeredAbilityImpl { - - UnsettledMarinerTriggeredAbility() { - super(Zone.BATTLEFIELD, null); - } - - private UnsettledMarinerTriggeredAbility(final UnsettledMarinerTriggeredAbility ability) { - super(ability); - } - - @Override - public UnsettledMarinerTriggeredAbility copy() { - return new UnsettledMarinerTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.TARGETED; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (!game.getOpponents(getControllerId()).contains(event.getPlayerId())) { - return false; - } - Permanent permanent = game.getPermanent(event.getTargetId()); - if ((permanent == null || !permanent.getControllerId().equals(getControllerId())) - && !event.getTargetId().equals(getControllerId())) { - return false; - } - Effect effect = new CounterUnlessPaysEffect(new GenericManaCost(1)); - effect.setTargetPointer(new FixedTarget(event.getSourceId(), game)); - this.getEffects().clear(); - this.addEffect(effect); - return true; - } - - @Override - public String getRule() { - return "Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, " + - "counter that spell or ability unless its controller pays {1}."; - } -} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/v/VaziKeenNegotiator.java b/Mage.Sets/src/mage/cards/v/VaziKeenNegotiator.java new file mode 100644 index 0000000000..84a2921bed --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/VaziKeenNegotiator.java @@ -0,0 +1,132 @@ +package mage.cards.v; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.condition.common.TreasureSpentToCastCondition; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.CreateTokenTargetEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.token.TreasureToken; +import mage.players.Player; +import mage.target.common.TargetOpponent; +import mage.watchers.common.CreatedTokenWatcher; +import mage.watchers.common.ManaPaidSourceWatcher; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class VaziKeenNegotiator extends CardImpl { + + public VaziKeenNegotiator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{B}{R}{G}"); + + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.HUMAN, SubType.ADVISOR); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // {T}: Target opponent creates X Treasure tokens, where X is the number of Treasure tokens you created this turn. + Ability tapAbility = new SimpleActivatedAbility(new CreateTokenTargetEffect(new TreasureToken(), VaziKeenNegotiatorNumberOfTokensCreated.instance), new TapSourceCost()); + tapAbility.addTarget(new TargetOpponent()); + this.addAbility(tapAbility); + + // Whenever an opponent casts a spell or activates an ability, + // if mana from a Treasure was spent to cast it or activate it, + // put a +1/+1 counter on target creature, + // then draw a card. + Ability castAbility = new VaziKeenNegotiatorOpponentCastsOrActivatesTriggeredAbility(); + castAbility.addTarget(new TargetOpponent()); + castAbility.addEffect(new DrawCardSourceControllerEffect(1)); + this.addAbility(castAbility); + } + + private VaziKeenNegotiator(final VaziKeenNegotiator card) { + super(card); + } + + @Override + public VaziKeenNegotiator copy() { + return new VaziKeenNegotiator(this); + } +} + +class VaziKeenNegotiatorOpponentCastsOrActivatesTriggeredAbility extends TriggeredAbilityImpl { + + VaziKeenNegotiatorOpponentCastsOrActivatesTriggeredAbility() { + super(Zone.BATTLEFIELD, new AddCountersTargetEffect(CounterType.P1P1.createInstance())); + setTriggerPhrase("Whenever an opponent casts a spell or activates an ability, if mana from a Treasure was spent to cast it or activate it, "); + } + + private VaziKeenNegotiatorOpponentCastsOrActivatesTriggeredAbility(final VaziKeenNegotiatorOpponentCastsOrActivatesTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST + || event.getType() == GameEvent.EventType.ACTIVATED_ABILITY; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Player controller = game.getPlayer(getControllerId()); + Player caster = game.getPlayer(event.getPlayerId()); + Optional optionalAbility = game.getAbility(event.getTargetId(), this.sourceId); + if (controller == null + || caster == null + || !game.getOpponents(controller.getId()).contains(caster.getId()) + || !optionalAbility.isPresent()) { + return false; + } + return TreasureSpentToCastCondition.instance.apply(game, optionalAbility.get()); + } + + @Override + public TriggeredAbility copy() { + return new VaziKeenNegotiatorOpponentCastsOrActivatesTriggeredAbility(this); + } +} + +enum VaziKeenNegotiatorNumberOfTokensCreated implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return CreatedTokenWatcher.getTypeCreatedCountByPlayer(sourceAbility.getControllerId(), TreasureToken.class, game); + } + + @Override + public DynamicValue copy() { + return instance; + } + + @Override + public String getMessage() { + return "the number of Treasure tokens you created this turn"; + } + + @Override + public String toString() { + return "X"; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/w/WaveOfRats.java b/Mage.Sets/src/mage/cards/w/WaveOfRats.java new file mode 100644 index 0000000000..345cc03baf --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WaveOfRats.java @@ -0,0 +1,72 @@ +package mage.cards.w; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DiesSourceTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.ReturnToBattlefieldUnderOwnerControlSourceEffect; +import mage.abilities.keyword.BlitzAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.watchers.common.DamageDoneWatcher; + +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public class WaveOfRats extends CardImpl { + + public WaveOfRats(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{B}"); + + this.addSubType(SubType.RAT); + this.power = new MageInt(4); + this.toughness = new MageInt(2); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // When Wave of Rats dies, if it dealt combat damage to a player this turn, return it to the battlefield under its owner’s control. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new DiesSourceTriggeredAbility(new ReturnToBattlefieldUnderOwnerControlSourceEffect()), + WaveOfRatsDealtDamageToPlayerCondition.instance, + "When Wave of Rats dies, if it dealt combat damage to a player this turn, return it to the battlefield under its owner's control.") + ); + + // Blitz {4}{B} (If you cast this spell for its blitz cost, it gains haste and “When this creature dies, draw a card.” Sacrifice it at the beginning of the next end step.) + this.addAbility(new BlitzAbility(this, "{4}{B}")); + } + + private WaveOfRats(final WaveOfRats card) { + super(card); + } + + @Override + public WaveOfRats copy() { + return new WaveOfRats(this); + } +} + +enum WaveOfRatsDealtDamageToPlayerCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + DamageDoneWatcher watcher = game.getState().getWatcher(DamageDoneWatcher.class); + Permanent waveOfRats = game.getPermanent(source.getSourceId()); + if (watcher == null || waveOfRats == null) { + return false; + } + if (watcher.damageDoneBy(waveOfRats.getId(), waveOfRats.getZoneChangeCounter(game), game) < 1) { + return false; + } + return watcher.damagedAPlayer(waveOfRats.getId(), waveOfRats.getZoneChangeCounter(game), game); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/w/WeatheredSentinels.java b/Mage.Sets/src/mage/cards/w/WeatheredSentinels.java new file mode 100644 index 0000000000..4c8bd1a37f --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WeatheredSentinels.java @@ -0,0 +1,234 @@ +package mage.cards.w; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.combat.CanAttackAsThoughItDidntHaveDefenderSourceEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.watchers.Watcher; + +import java.util.*; + +/** + * Based on Backstreet Bruiser, O-Kagachi, and Pramikon + * @author Alex-Vasile + */ +public class WeatheredSentinels extends CardImpl { + + public WeatheredSentinels(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}"); + + addSubType(SubType.WALL); + this.power = new MageInt(2); + this.toughness = new MageInt(5); + + // Defender, vigilance, reach, trample + this.addAbility(DefenderAbility.getInstance()); + this.addAbility(VigilanceAbility.getInstance()); + this.addAbility(ReachAbility.getInstance()); + this.addAbility(TrampleAbility.getInstance()); + + // Weathered Sentinels can attack players who attacked you during their last turn as though it didn't have defender. + this.addAbility(new SimpleStaticAbility( + new ConditionalAsThoughEffect( + new CanAttackAsThoughItDidntHaveDefenderSourceEffect(Duration.WhileOnBattlefield), + WeatheredSentinelsCanAttackSomeoneCondition.instance) + .setText("Weathered Sentinels can attack players who attacked you during their last turn as though it didn't have defender.")), + new WeatheredSentinelsLastTurnAttackersWatcher() + ); + + // Whenever Weathered Sentinels attacks, it gets +3/+3 and gains indestructible until end of turn. + Ability ability = new AttacksTriggeredAbility( + new BoostSourceEffect(3, 3, Duration.EndOfTurn).setText("it gets +3/+3") + ); + ability.addEffect(new GainAbilitySourceEffect( + IndestructibleAbility.getInstance(), Duration.EndOfTurn + ).concatBy("and").setText("gains indestructible until end of turn") + ); + this.addAbility(ability); + + // Ability to limit who Weathered Sentinels can attack + this.addAbility(new SimpleStaticAbility( + new WeatheredSentinelsAttackerReplacementEffect() + ).addHint(WeatheredSentinelsPlayersWhoAttackedYouLastTurn.instance) + ); + } + + private WeatheredSentinels(final WeatheredSentinels card) { + super(card); + } + + @Override + public WeatheredSentinels copy() { + return new WeatheredSentinels(this); + } +} + +class WeatheredSentinelsAttackerReplacementEffect extends ReplacementEffectImpl { + + WeatheredSentinelsAttackerReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + } + + WeatheredSentinelsAttackerReplacementEffect(final WeatheredSentinelsAttackerReplacementEffect effect) { + super(effect); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + return true; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DECLARE_ATTACKER; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + Player attacker = game.getPlayer(event.getPlayerId()); + if (controller == null || attacker == null) { + return false; + } + + if (!attacker.equals(controller)) { + return false; + } + + Player defender; + if (game.getPlayer(event.getTargetId()) != null) { + defender = game.getPlayer(event.getTargetId()); + } else { + Permanent planeswalker = game.getPermanent(event.getTargetId()); + defender = (planeswalker == null) ? null : game.getPlayer(planeswalker.getControllerId()); + } + if (defender == null) { + return false; + } + + WeatheredSentinelsLastTurnAttackersWatcher watcher = game.getState().getWatcher(WeatheredSentinelsLastTurnAttackersWatcher.class); + if (watcher == null) { + return false; + } + + // Attacker and defender are supposed to be flipped here + return watcher.checkPlayer(defender.getId(), attacker.getId()); + } + + @Override + public ContinuousEffect copy() { + return new WeatheredSentinelsAttackerReplacementEffect(this); + } +} + +enum WeatheredSentinelsPlayersWhoAttackedYouLastTurn implements Hint { + instance; + + @Override + public String getText(Game game, Ability ability) { + Player controller = game.getPlayer(ability.getControllerId()); + WeatheredSentinelsLastTurnAttackersWatcher watcher = game.getState().getWatcher(WeatheredSentinelsLastTurnAttackersWatcher.class); + if (controller == null || watcher == null) { + return ""; + } + + StringBuilder stringBuilder = new StringBuilder("Attacked you on their last turn: "); + Iterator opponentIdIterator = game.getOpponents(controller.getId()).iterator(); + + while (opponentIdIterator.hasNext()) { + UUID opponentId = opponentIdIterator.next(); + Player opponent = game.getPlayer(opponentId); + if (opponent != null && watcher.checkPlayer(opponentId, controller.getId())) { + stringBuilder.append(opponent.getName()); + // Add a ", " between names, but exclude adding one at the end + if (opponentIdIterator.hasNext()) { + stringBuilder.append(", "); + } else { + stringBuilder.append('.'); + } + } + } + + return stringBuilder.toString(); + } + + @Override + public Hint copy() { + return instance; + } +} + +enum WeatheredSentinelsCanAttackSomeoneCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + WeatheredSentinelsLastTurnAttackersWatcher watcher = game.getState().getWatcher(WeatheredSentinelsLastTurnAttackersWatcher.class); + if (controller == null || watcher == null) { + return false; + } + + for (UUID opponentId : game.getOpponents(controller.getId())) { + Player opponent = game.getPlayer(opponentId); + if (opponent != null && watcher.checkPlayer(controller.getId(), opponentId)) { + return true; + } + } + return false; + } +} + +class WeatheredSentinelsLastTurnAttackersWatcher extends Watcher { + + private final Map> playerMap = new HashMap<>(); + + WeatheredSentinelsLastTurnAttackersWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + switch (event.getType()) { + case BEGINNING_PHASE_PRE: + playerMap.remove(game.getActivePlayerId()); + return; + case ATTACKER_DECLARED: + UUID attacker = event.getPlayerId(); + Set defenders = playerMap.getOrDefault(attacker, new HashSet<>()); + defenders.add(event.getTargetId()); + playerMap.put(attacker, defenders); + } + } + + /** + * Checks if on attackerId's last turn they attacked defenderId. + * + * @param attackerId The ID of the player to see if they attacked the given defender on the attacker's last turn + * @param defenderId The ID of the player to see if they were attacked by the attacker on the attacker's last turn + * @return Whether the attacker attacked the defender on the attacker's last turn + */ + boolean checkPlayer(UUID attackerId, UUID defenderId) { + if (attackerId == null || defenderId == null) { + return false; + } + Set defendersLastTurn = playerMap.get(defenderId); + return defendersLastTurn != null && defendersLastTurn.contains(attackerId); + } +} diff --git a/Mage.Sets/src/mage/cards/w/WildMagicSorcerer.java b/Mage.Sets/src/mage/cards/w/WildMagicSorcerer.java index 7b993b2941..fef3851fe4 100644 --- a/Mage.Sets/src/mage/cards/w/WildMagicSorcerer.java +++ b/Mage.Sets/src/mage/cards/w/WildMagicSorcerer.java @@ -70,23 +70,24 @@ class WildMagicSorcererGainCascadeFirstSpellCastFromExileEffect extends Continuo @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - for (StackObject stackObject : game.getStack()) { - // only spells cast, so no copies of spells - if ((stackObject instanceof Spell) - && !stackObject.isCopy() - && stackObject.isControlledBy(source.getControllerId())) { - Spell spell = (Spell) stackObject; - WildMagicSorcererWatcher watcher = game.getState().getWatcher(WildMagicSorcererWatcher.class); - if (watcher != null - && FirstSpellCastFromExileEachTurnCondition.instance.apply(game, source)) { - game.getState().addOtherAbility(spell.getCard(), cascadeAbility); - } + WildMagicSorcererWatcher watcher = game.getState().getWatcher(WildMagicSorcererWatcher.class); + if (controller == null || watcher == null) { + return false; + } + + for (StackObject stackObject : game.getStack()) { + // only spells cast, so no copies of spells + if ((stackObject instanceof Spell) + && !stackObject.isCopy() + && stackObject.isControlledBy(source.getControllerId())) { + Spell spell = (Spell) stackObject; + + if (FirstSpellCastFromExileEachTurnCondition.instance.apply(game, source)) { + game.getState().addOtherAbility(spell.getCard(), cascadeAbility); } } - return true; } - return false; + return true; } } @@ -100,8 +101,7 @@ enum FirstSpellCastFromExileEachTurnCondition implements Condition { } WildMagicSorcererWatcher watcher = game.getState().getWatcher(WildMagicSorcererWatcher.class); StackObject so = game.getStack().getFirst(); - return so != null - && watcher != null + return watcher != null && WildMagicSorcererWatcher.checkSpell(so, game); } } diff --git a/Mage.Sets/src/mage/cards/x/XandersPact.java b/Mage.Sets/src/mage/cards/x/XandersPact.java index db9c911e03..d82be27809 100644 --- a/Mage.Sets/src/mage/cards/x/XandersPact.java +++ b/Mage.Sets/src/mage/cards/x/XandersPact.java @@ -32,7 +32,7 @@ public final class XandersPact extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{B}{B}"); // Casualty 2 - this.addAbility(new CasualtyAbility(this, 2)); + this.addAbility(new CasualtyAbility(2)); // Each opponent exiles the top card of their library. You may cast spells from among those cards this turn. If you cast a spell this way, pay life equal to that spell's mana value rather than pay its mana cost. this.getSpellAbility().addEffect(new XandersPactExileEffect()); diff --git a/Mage.Sets/src/mage/sets/NewCapennaCommander.java b/Mage.Sets/src/mage/sets/NewCapennaCommander.java index 8bffc8eaee..e6f98141b0 100644 --- a/Mage.Sets/src/mage/sets/NewCapennaCommander.java +++ b/Mage.Sets/src/mage/sets/NewCapennaCommander.java @@ -20,7 +20,10 @@ public final class NewCapennaCommander extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Aether Snap", 241, Rarity.RARE, mage.cards.a.AetherSnap.class)); + cards.add(new SetCardInfo("Aerial Extortionist", 11, Rarity.RARE, mage.cards.a.AerialExtortionist.class)); + cards.add(new SetCardInfo("Agent's Toolkit", 66, Rarity.RARE, mage.cards.a.AgentsToolkit.class)); cards.add(new SetCardInfo("Agitator Ant", 263, Rarity.RARE, mage.cards.a.AgitatorAnt.class)); + cards.add(new SetCardInfo("Anhelo, the Painter", 1, Rarity.MYTHIC, mage.cards.a.AnheloThePainter.class)); cards.add(new SetCardInfo("Ajani Unyielding", 324, Rarity.MYTHIC, mage.cards.a.AjaniUnyielding.class)); cards.add(new SetCardInfo("Alela, Artful Provocateur", 325, Rarity.MYTHIC, mage.cards.a.AlelaArtfulProvocateur.class)); cards.add(new SetCardInfo("Angelic Sleuth", 12, Rarity.RARE, mage.cards.a.AngelicSleuth.class)); @@ -49,6 +52,7 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Bedevil", 331, Rarity.RARE, mage.cards.b.Bedevil.class)); cards.add(new SetCardInfo("Bellowing Mauler", 33, Rarity.RARE, mage.cards.b.BellowingMauler.class)); cards.add(new SetCardInfo("Bennie Bracks, Zoologist", 86, Rarity.MYTHIC, mage.cards.b.BennieBracksZoologist.class)); + cards.add(new SetCardInfo("Bess, Soul Nourisher", 67, Rarity.RARE, mage.cards.b.BessSoulNourisher.class)); cards.add(new SetCardInfo("Blasphemous Act", 264, Rarity.RARE, mage.cards.b.BlasphemousAct.class)); cards.add(new SetCardInfo("Blighted Woodland", 388, Rarity.UNCOMMON, mage.cards.b.BlightedWoodland.class)); cards.add(new SetCardInfo("Bloodsoaked Champion", 243, Rarity.RARE, mage.cards.b.BloodsoakedChampion.class)); @@ -85,6 +89,7 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Crash the Party", 57, Rarity.RARE, mage.cards.c.CrashTheParty.class)); cards.add(new SetCardInfo("Creeping Tar Pit", 396, Rarity.RARE, mage.cards.c.CreepingTarPit.class)); cards.add(new SetCardInfo("Crumbling Necropolis", 397, Rarity.UNCOMMON, mage.cards.c.CrumblingNecropolis.class)); + cards.add(new SetCardInfo("Cryptic Pursuit", 70, Rarity.RARE, mage.cards.c.CrypticPursuit.class)); cards.add(new SetCardInfo("Crystalline Giant", 364, Rarity.RARE, mage.cards.c.CrystallineGiant.class)); cards.add(new SetCardInfo("Cultivate", 285, Rarity.UNCOMMON, mage.cards.c.Cultivate.class)); cards.add(new SetCardInfo("Currency Converter", 81, Rarity.RARE, mage.cards.c.CurrencyConverter.class)); @@ -98,6 +103,7 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Deathreap Ritual", 336, Rarity.UNCOMMON, mage.cards.d.DeathreapRitual.class)); cards.add(new SetCardInfo("Declaration in Stone", 196, Rarity.RARE, mage.cards.d.DeclarationInStone.class)); cards.add(new SetCardInfo("Deep Analysis", 218, Rarity.COMMON, mage.cards.d.DeepAnalysis.class)); + cards.add(new SetCardInfo("Denry Klin, Editor in Chief", 71, Rarity.RARE, mage.cards.d.DenryKlinEditorInChief.class)); cards.add(new SetCardInfo("Determined Iteration", 45, Rarity.RARE, mage.cards.d.DeterminedIteration.class)); cards.add(new SetCardInfo("Devoted Druid", 286, Rarity.UNCOMMON, mage.cards.d.DevotedDruid.class)); cards.add(new SetCardInfo("Dig Through Time", 219, Rarity.RARE, mage.cards.d.DigThroughTime.class)); @@ -123,6 +129,7 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Fact or Fiction", 221, Rarity.UNCOMMON, mage.cards.f.FactOrFiction.class)); cards.add(new SetCardInfo("Fallen Shinobi", 338, Rarity.RARE, mage.cards.f.FallenShinobi.class)); cards.add(new SetCardInfo("False Floor", 82, Rarity.RARE, mage.cards.f.FalseFloor.class)); + cards.add(new SetCardInfo("Family's Favor", 59, Rarity.RARE, mage.cards.f.FamilysFavor.class)); cards.add(new SetCardInfo("Farseek", 290, Rarity.COMMON, mage.cards.f.Farseek.class)); cards.add(new SetCardInfo("Fathom Mage", 339, Rarity.RARE, mage.cards.f.FathomMage.class)); cards.add(new SetCardInfo("Feed the Swarm", 250, Rarity.COMMON, mage.cards.f.FeedTheSwarm.class)); @@ -199,6 +206,7 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Magus of the Wheel", 271, Rarity.RARE, mage.cards.m.MagusOfTheWheel.class)); cards.add(new SetCardInfo("Make an Example", 37, Rarity.RARE, mage.cards.m.MakeAnExample.class)); cards.add(new SetCardInfo("March of the Multitudes", 346, Rarity.MYTHIC, mage.cards.m.MarchOfTheMultitudes.class)); + cards.add(new SetCardInfo("Mari, the Killing Quill", 89, Rarity.RARE, mage.cards.m.MariTheKillingQuill.class)); cards.add(new SetCardInfo("Martial Coup", 206, Rarity.RARE, mage.cards.m.MartialCoup.class)); cards.add(new SetCardInfo("Mask of Riddles", 347, Rarity.UNCOMMON, mage.cards.m.MaskOfRiddles.class)); cards.add(new SetCardInfo("Mask of the Schemer", 28, Rarity.RARE, mage.cards.m.MaskOfTheSchemer.class)); @@ -216,6 +224,7 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Nadir Kraken", 228, Rarity.RARE, mage.cards.n.NadirKraken.class)); cards.add(new SetCardInfo("Naya Panorama", 417, Rarity.COMMON, mage.cards.n.NayaPanorama.class)); cards.add(new SetCardInfo("Nesting Grounds", 418, Rarity.RARE, mage.cards.n.NestingGrounds.class)); + cards.add(new SetCardInfo("Next of Kin", 62, Rarity.RARE, mage.cards.n.NextOfKin.class)); cards.add(new SetCardInfo("Nightmare Unmaking", 253, Rarity.RARE, mage.cards.n.NightmareUnmaking.class)); cards.add(new SetCardInfo("Noxious Gearhulk", 254, Rarity.MYTHIC, mage.cards.n.NoxiousGearhulk.class)); cards.add(new SetCardInfo("Oblivion Stone", 373, Rarity.RARE, mage.cards.o.OblivionStone.class)); @@ -223,13 +232,16 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Oracle's Vault", 374, Rarity.RARE, mage.cards.o.OraclesVault.class)); cards.add(new SetCardInfo("Orzhov Advokist", 207, Rarity.UNCOMMON, mage.cards.o.OrzhovAdvokist.class)); cards.add(new SetCardInfo("Orzhov Signet", 375, Rarity.UNCOMMON, mage.cards.o.OrzhovSignet.class)); + cards.add(new SetCardInfo("Oskar, Rubbish Reclaimer", 77, Rarity.RARE, mage.cards.o.OskarRubbishReclaimer.class)); cards.add(new SetCardInfo("Outpost Siege", 272, Rarity.RARE, mage.cards.o.OutpostSiege.class)); cards.add(new SetCardInfo("Overgrown Battlement", 303, Rarity.UNCOMMON, mage.cards.o.OvergrownBattlement.class)); cards.add(new SetCardInfo("Painful Truths", 255, Rarity.RARE, mage.cards.p.PainfulTruths.class)); cards.add(new SetCardInfo("Park Heights Maverick", 63, Rarity.RARE, mage.cards.p.ParkHeightsMaverick.class)); + cards.add(new SetCardInfo("Parnesse, the Subtle Brush", 8, Rarity.MYTHIC, mage.cards.p.ParnesseTheSubtleBrush.class)); cards.add(new SetCardInfo("Path of Ancestry", 419, Rarity.COMMON, mage.cards.p.PathOfAncestry.class)); cards.add(new SetCardInfo("Path to Exile", 208, Rarity.UNCOMMON, mage.cards.p.PathToExile.class)); cards.add(new SetCardInfo("Perrie, the Pulverizer", 5, Rarity.MYTHIC, mage.cards.p.PerrieThePulverizer.class)); + cards.add(new SetCardInfo("Phabine, Boss's Confidant", 9, Rarity.MYTHIC, mage.cards.p.PhabineBosssConfidant.class)); cards.add(new SetCardInfo("Planar Outburst", 209, Rarity.RARE, mage.cards.p.PlanarOutburst.class)); cards.add(new SetCardInfo("Ponder", 229, Rarity.COMMON, mage.cards.p.Ponder.class)); cards.add(new SetCardInfo("Port Town", 420, Rarity.RARE, mage.cards.p.PortTown.class)); @@ -242,14 +254,17 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Protection Racket", 39, Rarity.RARE, mage.cards.p.ProtectionRacket.class)); cards.add(new SetCardInfo("Puppeteer Clique", 257, Rarity.RARE, mage.cards.p.PuppeteerClique.class)); cards.add(new SetCardInfo("Quietus Spike", 377, Rarity.RARE, mage.cards.q.QuietusSpike.class)); + cards.add(new SetCardInfo("Rain of Riches", 50, Rarity.RARE, mage.cards.r.RainOfRiches.class)); cards.add(new SetCardInfo("Rakdos Signet", 378, Rarity.UNCOMMON, mage.cards.r.RakdosSignet.class)); cards.add(new SetCardInfo("Rampant Growth", 304, Rarity.COMMON, mage.cards.r.RampantGrowth.class)); cards.add(new SetCardInfo("Reign of the Pit", 258, Rarity.RARE, mage.cards.r.ReignOfThePit.class)); cards.add(new SetCardInfo("Rekindling Phoenix", 273, Rarity.MYTHIC, mage.cards.r.RekindlingPhoenix.class)); + cards.add(new SetCardInfo("Resourceful Defense", 19, Rarity.RARE, mage.cards.r.ResourcefulDefense.class)); cards.add(new SetCardInfo("Rishkar's Expertise", 306, Rarity.RARE, mage.cards.r.RishkarsExpertise.class)); cards.add(new SetCardInfo("Rishkar, Peema Renegade", 305, Rarity.RARE, mage.cards.r.RishkarPeemaRenegade.class)); cards.add(new SetCardInfo("Rite of the Raging Storm", 274, Rarity.UNCOMMON, mage.cards.r.RiteOfTheRagingStorm.class)); cards.add(new SetCardInfo("River's Rebuke", 231, Rarity.RARE, mage.cards.r.RiversRebuke.class)); + cards.add(new SetCardInfo("Riveteers Confluence", 79, Rarity.RARE, mage.cards.r.RiveteersConfluence.class)); cards.add(new SetCardInfo("Roalesk, Apex Hybrid", 349, Rarity.MYTHIC, mage.cards.r.RoaleskApexHybrid.class)); cards.add(new SetCardInfo("Rogue's Passage", 422, Rarity.UNCOMMON, mage.cards.r.RoguesPassage.class)); cards.add(new SetCardInfo("Rose Room Treasurer", 51, Rarity.RARE, mage.cards.r.RoseRoomTreasurer.class)); @@ -267,15 +282,19 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Shadowblood Ridge", 426, Rarity.RARE, mage.cards.s.ShadowbloodRidge.class)); cards.add(new SetCardInfo("Shadowmage Infiltrator", 351, Rarity.UNCOMMON, mage.cards.s.ShadowmageInfiltrator.class)); cards.add(new SetCardInfo("Shamanic Revelation", 311, Rarity.RARE, mage.cards.s.ShamanicRevelation.class)); + cards.add(new SetCardInfo("Shield Broker", 29, Rarity.RARE, mage.cards.s.ShieldBroker.class)); cards.add(new SetCardInfo("Silent-Blade Oni", 352, Rarity.RARE, mage.cards.s.SilentBladeOni.class)); + cards.add(new SetCardInfo("Sinister Concierge", 30, Rarity.RARE, mage.cards.s.SinisterConcierge.class)); cards.add(new SetCardInfo("Skyboon Evangelist", 20, Rarity.RARE, mage.cards.s.SkyboonEvangelist.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Skyboon Evangelist", 121, Rarity.RARE, mage.cards.s.SkyboonEvangelist.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Skyclave Shade", 260, Rarity.RARE, mage.cards.s.SkyclaveShade.class)); cards.add(new SetCardInfo("Skycloud Expanse", 427, Rarity.RARE, mage.cards.s.SkycloudExpanse.class)); cards.add(new SetCardInfo("Skyship Plunderer", 232, Rarity.UNCOMMON, mage.cards.s.SkyshipPlunderer.class)); + cards.add(new SetCardInfo("Skyway Robber", 31, Rarity.RARE, mage.cards.s.SkywayRobber.class)); cards.add(new SetCardInfo("Slippery Bogbonder", 312, Rarity.RARE, mage.cards.s.SlipperyBogbonder.class)); cards.add(new SetCardInfo("Smoldering Marsh", 428, Rarity.RARE, mage.cards.s.SmolderingMarsh.class)); cards.add(new SetCardInfo("Smuggler's Share", 21, Rarity.RARE, mage.cards.s.SmugglersShare.class)); + cards.add(new SetCardInfo("Smuggler's Buggy", 84, Rarity.RARE, mage.cards.s.SmugglersBuggy.class)); cards.add(new SetCardInfo("Sol Ring", 379, Rarity.UNCOMMON, mage.cards.s.SolRing.class)); cards.add(new SetCardInfo("Solemn Simulacrum", 380, Rarity.RARE, mage.cards.s.SolemnSimulacrum.class)); cards.add(new SetCardInfo("Spellbinding Soprano", 53, Rarity.RARE, mage.cards.s.SpellbindingSoprano.class)); @@ -287,10 +306,12 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Stolen Identity", 233, Rarity.RARE, mage.cards.s.StolenIdentity.class)); cards.add(new SetCardInfo("Storm of Forms", 32, Rarity.RARE, mage.cards.s.StormOfForms.class)); cards.add(new SetCardInfo("Strionic Resonator", 381, Rarity.RARE, mage.cards.s.StrionicResonator.class)); + cards.add(new SetCardInfo("Syrix, Carrier of the Flame", 80, Rarity.RARE, mage.cards.s.SyrixCarrierOfTheFlame.class)); cards.add(new SetCardInfo("Sun Titan", 210, Rarity.MYTHIC, mage.cards.s.SunTitan.class)); cards.add(new SetCardInfo("Sungrass Prairie", 430, Rarity.RARE, mage.cards.s.SungrassPrairie.class)); cards.add(new SetCardInfo("Sunken Hollow", 431, Rarity.RARE, mage.cards.s.SunkenHollow.class)); cards.add(new SetCardInfo("Swiftfoot Boots", 382, Rarity.UNCOMMON, mage.cards.s.SwiftfootBoots.class)); + cards.add(new SetCardInfo("Swindler's Scheme", 88, Rarity.RARE, mage.cards.s.SwindlersScheme.class)); cards.add(new SetCardInfo("Swords to Plowshares", 211, Rarity.UNCOMMON, mage.cards.s.SwordsToPlowshares.class)); cards.add(new SetCardInfo("Sylvan Offering", 314, Rarity.RARE, mage.cards.s.SylvanOffering.class)); cards.add(new SetCardInfo("Talrand's Invocation", 234, Rarity.UNCOMMON, mage.cards.t.TalrandsInvocation.class)); @@ -301,11 +322,13 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Temple of Triumph", 437, Rarity.RARE, mage.cards.t.TempleOfTriumph.class)); cards.add(new SetCardInfo("Temple of the False God", 436, Rarity.UNCOMMON, mage.cards.t.TempleOfTheFalseGod.class)); cards.add(new SetCardInfo("Temur Sabertooth", 315, Rarity.UNCOMMON, mage.cards.t.TemurSabertooth.class)); + cards.add(new SetCardInfo("Tenuous Truce", 87, Rarity.RARE, mage.cards.t.TenuousTruce.class)); cards.add(new SetCardInfo("Terminate", 353, Rarity.UNCOMMON, mage.cards.t.Terminate.class)); cards.add(new SetCardInfo("Tezzeret's Gambit", 235, Rarity.RARE, mage.cards.t.TezzeretsGambit.class)); cards.add(new SetCardInfo("The Beamtown Bullies", 6, Rarity.MYTHIC, mage.cards.t.TheBeamtownBullies.class)); cards.add(new SetCardInfo("Thief of Sanity", 354, Rarity.RARE, mage.cards.t.ThiefOfSanity.class)); cards.add(new SetCardInfo("Thragtusk", 316, Rarity.RARE, mage.cards.t.Thragtusk.class)); + cards.add(new SetCardInfo("Threefold Signal", 93, Rarity.MYTHIC, mage.cards.t.ThreefoldSignal.class)); cards.add(new SetCardInfo("Thriving Bluff", 438, Rarity.COMMON, mage.cards.t.ThrivingBluff.class)); cards.add(new SetCardInfo("Thriving Grove", 439, Rarity.COMMON, mage.cards.t.ThrivingGrove.class)); cards.add(new SetCardInfo("Thriving Heath", 440, Rarity.COMMON, mage.cards.t.ThrivingHeath.class)); @@ -322,6 +345,7 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Twinning Staff", 383, Rarity.RARE, mage.cards.t.TwinningStaff.class)); cards.add(new SetCardInfo("Urban Evolution", 355, Rarity.UNCOMMON, mage.cards.u.UrbanEvolution.class)); cards.add(new SetCardInfo("Utter End", 356, Rarity.RARE, mage.cards.u.UtterEnd.class)); + cards.add(new SetCardInfo("Vazi, Keen Negotiator", 92, Rarity.RARE, mage.cards.v.VaziKeenNegotiator.class)); cards.add(new SetCardInfo("Victimize", 261, Rarity.UNCOMMON, mage.cards.v.Victimize.class)); cards.add(new SetCardInfo("Vivid Creek", 444, Rarity.UNCOMMON, mage.cards.v.VividCreek.class)); cards.add(new SetCardInfo("Vivid Grove", 445, Rarity.UNCOMMON, mage.cards.v.VividGrove.class)); @@ -331,7 +355,9 @@ public final class NewCapennaCommander extends ExpansionSet { cards.add(new SetCardInfo("Wall of Roots", 319, Rarity.COMMON, mage.cards.w.WallOfRoots.class)); cards.add(new SetCardInfo("Warstorm Surge", 277, Rarity.RARE, mage.cards.w.WarstormSurge.class)); cards.add(new SetCardInfo("Waste Management", 40, Rarity.RARE, mage.cards.w.WasteManagement.class)); + cards.add(new SetCardInfo("Wave of Rats", 41, Rarity.RARE, mage.cards.w.WaveOfRats.class)); cards.add(new SetCardInfo("Wayfarer's Bauble", 384, Rarity.COMMON, mage.cards.w.WayfarersBauble.class)); + cards.add(new SetCardInfo("Weathered Sentinels", 85, Rarity.RARE, mage.cards.w.WeatheredSentinels.class)); cards.add(new SetCardInfo("Whirler Rogue", 238, Rarity.UNCOMMON, mage.cards.w.WhirlerRogue.class)); cards.add(new SetCardInfo("Wickerbough Elder", 320, Rarity.COMMON, mage.cards.w.WickerboughElder.class)); cards.add(new SetCardInfo("Windbrisk Heights", 447, Rarity.RARE, mage.cards.w.WindbriskHeights.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CasualtyTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CasualtyTest.java new file mode 100644 index 0000000000..22d6d3136b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/CasualtyTest.java @@ -0,0 +1,110 @@ +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.CardTestPlayerBase; + +/** + * @author Alex-Vasile + */ +public class CasualtyTest extends CardTestPlayerBase { + + // Instant + // {1}{U} + // Casualty 1 + // Look at the top two cards of your library. Put one of them into your hand and the other on the bottom of your library. + private static final String aLittleChat = "A Little Chat"; + // Planeswalker + // {1}{B}{R} + // Casualty X + // The copy isn’t legendary and has starting loyalty X. + // −7: Target player draws seven cards and loses 7 life. + private static final String obNixilisTheAdversary = "Ob Nixilis, the Adversary"; + // 7/7 used as casualty + private static final String aetherwindBasker = "Aetherwind Basker"; + + /** + * Test Casualty on sorcery/instant. + */ + @Test + public void testCasualtySorceryInstant() { + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.HAND, playerA, aLittleChat); + addCard(Zone.LIBRARY, playerA, "Desert", 4); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aLittleChat); + setChoice(playerA, "Yes"); + setChoice(playerA, aetherwindBasker); + addTarget(playerA, "Desert"); + addTarget(playerA, "Desert"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertHandCount(playerA, "Desert", 2); + assertGraveyardCount(playerA, aetherwindBasker, 1); + } + + /** + * Test that casualty will only let you pay it once. + */ + @Test + public void testCanOnlyPayCasualtyOnce() { + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker, 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.HAND, playerA, aLittleChat); + addCard(Zone.LIBRARY, playerA, "Desert", 4); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aLittleChat); + setChoice(playerA, "Yes"); + setChoice(playerA, aetherwindBasker); + // If a second target was possible, it would have prompted us for another and this test would fail when strict choose mode was on + addTarget(playerA, "Desert"); + addTarget(playerA, "Desert"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, "Desert", 2); + assertGraveyardCount(playerA, aetherwindBasker, 1); + assertPermanentCount(playerA, aetherwindBasker, 1); + } + + /** + * Test Casualty on a creature. + * Test variable casualty. + */ + @Test + public void testVariableCasualtyOnCreature() { + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, obNixilisTheAdversary); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, obNixilisTheAdversary); + setChoice(playerA, aetherwindBasker); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertGraveyardCount(playerA, aetherwindBasker, 1); + assertPermanentCount(playerA, obNixilisTheAdversary, 2); // 2 were created, but the token died when using its -7 ability + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "-7:"); // -7 life and draw 7 cards + addTarget(playerA, playerA); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertPermanentCount(playerA, obNixilisTheAdversary, 1); + assertLife(playerA, 20 - 7); + assertHandCount(playerA, 7); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/PutCardFromOneOfTwoZonesOntoBattlefieldEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/PutCardFromOneOfTwoZonesOntoBattlefieldEffectTest.java new file mode 100644 index 0000000000..95e717cbf5 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/oneshot/PutCardFromOneOfTwoZonesOntoBattlefieldEffectTest.java @@ -0,0 +1,192 @@ +package org.mage.test.cards.abilities.oneshot; + +import mage.abilities.keyword.HasteAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Alex-Vasile + */ +public class PutCardFromOneOfTwoZonesOntoBattlefieldEffectTest extends CardTestPlayerBase { + + // −5: You may put a creature card with mana value less than or equal to the number of lands you control onto the battlefield from your hand or graveyard with two +1/+1 counters on it. + private static final String nissa = "Nissa of Shadowed Boughs"; + + // {4}{B}{R} + // When Swift Warkite enters the battlefield, you may put a creature card with mana value 3 or less from your hand or graveyard onto the battlefield. + // That creature gains haste. + // Return it to your hand at the beginning of the next end step. + private static final String swift = "Swift Warkite"; + + // Simple 1/1 for Swift Warkite to put on the battlefield with its ETB + private static final String sliver = "Metallic Sliver"; + + // −2: You may put an Equipment card from your hand or graveyard onto the battlefield. + private static final String nahiri = "Nahiri, the Lithomancer"; + + // Equipment cards for Nahiri + private static final String vorpal = "Vorpal Sword"; + private static final String axe = "Bloodforged Battle-Axe"; + + /** + * Test with no matching cards in hand or graveyard. + */ + @Test + public void testNoMatches() { + addCard(Zone.BATTLEFIELD, playerA, nahiri); + + setStrictChooseMode(true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-2"); + // The player should not be prompted for a choice + + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + /** + * Test with matching cards only in graveyard. + */ + @Test + public void testOnlyGraveyardHasMatches() { + addCard(Zone.BATTLEFIELD, playerA, nahiri); + addCard(Zone.GRAVEYARD, playerA, vorpal); + + setStrictChooseMode(true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-2"); + setChoice(playerA, vorpal); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, vorpal, 1); + } + + /** + * Test with matching cards only in hand. + */ + @Test + public void testOnlyHandHasMatches() { + addCard(Zone.BATTLEFIELD, playerA, nahiri); + addCard(Zone.HAND, playerA, vorpal); + + setStrictChooseMode(true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-2"); + setChoice(playerA, vorpal); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, vorpal, 1); + } + + /** + * Test with matching cards in both hand and graveyard. + */ + @Test + public void testBothHandAndGraveyardHaveMatches() { + addCard(Zone.BATTLEFIELD, playerA, nahiri); + addCard(Zone.HAND, playerA, vorpal); + addCard(Zone.GRAVEYARD, playerA, axe); + + setStrictChooseMode(true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-2"); + setChoice(playerA, "Yes"); // For player this choice is "Hand" but tests require "Yes" + setChoice(playerA, vorpal); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, vorpal, 1); + } + + /** + * Test {@link mage.cards.n.NissaOfShadowedBoughs Nissa of Shadowed Boughs} + * Starting loyalty = 4 + * You may put a creature card with mana value less than or equal to the number of lands you control + * onto the battlefield from your hand or graveyard + * with two +1/+1 counters on it. + */ + @Test + public void testNissaCanPlay() { + addCard(Zone.BATTLEFIELD, playerA, nissa); + addCard(Zone.HAND, playerA, swift); // {4}{B}{R} + addCard(Zone.HAND, playerA, "Mountain", 5); + addCard(Zone.HAND, playerA, "Mountain"); + + setStrictChooseMode(true); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain"); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "-5"); + setChoice(playerA, swift); + setChoice(playerA, "Yes"); // Say yes to Swift Warkite's ETB (no further choice needed since there are no possible options + + setStopAt(1, PhaseStep.END_TURN); + + execute(); + + assertGraveyardCount(playerA, nissa, 1); + assertPermanentCount(playerA, swift, 1); + assertCounterCount(swift, CounterType.P1P1, 2); + } + + /** + * Test {@link mage.cards.n.NissaOfShadowedBoughs Nissa of Shadowed Boughs} + * Starting loyalty = 4 + * You may put a creature card with mana value less than or equal to the number of lands you control + * onto the battlefield from your hand or graveyard + * with two +1/+1 counters on it. + */ + @Test + public void testNissaCantPlay() { + addCard(Zone.BATTLEFIELD, playerA, nissa); + addCard(Zone.HAND, playerA, swift); // {4}{B}{R} + addCard(Zone.HAND, playerA, "Mountain"); + + + setStrictChooseMode(true); + + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain"); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "-5"); + + setStopAt(1, PhaseStep.END_TURN); + + execute(); + + assertGraveyardCount(playerA, nissa, 1); + } + + /** + * Test {@link mage.cards.s.SwiftWarkite Swift Warkite} + * When Swift Warkite enters the battlefield, you may put a creature card with converted mana cost 3 or less from your hand or graveyard onto the battlefield. + * That creature gains haste. + * Return it to your hand at the beginning of the next end step. + */ + @Test + public void testSwiftWarkite() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.HAND, playerA, swift); + addCard(Zone.HAND, playerA, sliver); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, swift); + setChoice(playerA, "Yes"); // Yes to activating Swift Warkite's ETB + setChoice(playerA, sliver); // Pick the sliver for the ETB + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, sliver, 1); + assertAbility(playerA, sliver, HasteAbility.getInstance(), true); + + setStopAt(2, PhaseStep.PRECOMBAT_MAIN); + + execute(); + + assertHandCount(playerA, sliver, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/gpt/DjinnIlluminatusTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/gpt/DjinnIlluminatusTest.java new file mode 100644 index 0000000000..ffed8e736a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/gpt/DjinnIlluminatusTest.java @@ -0,0 +1,79 @@ +package org.mage.test.cards.single.gpt; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * {@link mage.cards.d.DjinnIlluminatus Djinn Illuminatus} + *

+ * Each instant and sorcery spell you cast has replicate. + * The replicate cost is equal to its mana cost. + * (When you cast it, copy it for each time you paid its replicate cost. You may choose new targets for the copies.) + * + * @author Alex-Vasile + */ +public class DjinnIlluminatusTest extends CardTestPlayerBase { + + private static final String djinnIlluminatus = "Djinn Illuminatus"; + private static final String lightningBolt = "Lightning Bolt"; + private static final String mountain = "Mountain"; + + /** + * Test that it works for you spells on your turn. + */ + @Test + public void testYourSpellYourTurn() { + addCard(Zone.BATTLEFIELD, playerA, djinnIlluminatus); + addCard(Zone.BATTLEFIELD, playerA, mountain, 2); + addCard(Zone.HAND, playerA, lightningBolt); + + setStrictChooseMode(true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, playerB); + setChoice(playerA, "Yes"); // Replicate + setChoice(playerA, "No"); // Only replicate once + setChoice(playerA, "No"); // Don't change the target + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertLife(playerB, 14); + } + + /** + * Test that it works for your spell on other's turn. + */ + @Test + public void testYourSpellNotYourTurn() { + addCard(Zone.BATTLEFIELD, playerA, djinnIlluminatus); + addCard(Zone.BATTLEFIELD, playerA, mountain, 2); + addCard(Zone.HAND, playerA, lightningBolt); + + setStrictChooseMode(true); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, playerB); + setChoice(playerA, "Yes"); // Replicate + setChoice(playerA, "No"); // Only replicate once + setChoice(playerA, "No"); // Don't change the target + + setStopAt(2, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertLife(playerB, 14); + } + + /** + * Test that it does not copy other's spell. + */ + @Test + public void testOthersSpell() { + addCard(Zone.BATTLEFIELD, playerA, djinnIlluminatus); + addCard(Zone.BATTLEFIELD, playerB, mountain, 2); + addCard(Zone.HAND, playerB, lightningBolt); + + setStrictChooseMode(true); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, lightningBolt, playerA); + + setStopAt(2, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertLife(playerA, 17); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/AnheloTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/AnheloTest.java new file mode 100644 index 0000000000..572f79c12b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/AnheloTest.java @@ -0,0 +1,198 @@ +package org.mage.test.cards.single.ncc; + +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import java.util.UUID; + +/** + * {@link mage.cards.a.AnheloThePainter Anhelo, the Painter} + * The first instant or sorcery spell you cast each turn has casualty 2. + * (As you cast that spell, you may sacrifice a creature with power 2 or greater. + * When you do, copy the spell and you may choose new targets for the copy.) + * + * @author Alex-Vasile + */ +public class AnheloTest extends CardTestPlayerBase { + + private static final String anhelo = "Anhelo, the Painter"; // {U}{B}{R} + private static final String lightningBolt = "Lightning Bolt"; // {R} + private static final String solRing = "Sol Ring"; // {1} + private static final String mountain = "Mountain"; + // MDFC Creature—Instant + private static final String flamescrollCelebrant = "Flamescroll Celebrant"; // {1}{R} + private static final String revelInSilence = "Revel in Silence"; // {W}{W} + // 7/7 used as casualty + private static final String aetherwindBasker = "Aetherwind Basker"; + // Instant + // {1}{U} + // Casualty 1 + // Look at the top two cards of your library. Put one of them into your hand and the other on the bottom of your library. + private static final String aLittleChat = "A Little Chat"; + + /** + * Test that it works for sorcery, but only the first one. + */ + @Test + public void testWorksForFirstOnly() { + addCard(Zone.BATTLEFIELD, playerA, anhelo); + addCard(Zone.BATTLEFIELD, playerA, mountain, 2); + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker, 2); + addCard(Zone.HAND, playerA, lightningBolt, 2); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, playerB); + setChoice(playerA, "Yes"); // Cast with Casualty + setChoice(playerA, aetherwindBasker); + setChoice(playerA, "No"); // Don't change targets + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, playerB); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertLife(playerB, 20 - 2*3 - 3); + assertPermanentCount(playerA, aetherwindBasker, 1); + } + + /** + * Test that it does not trigger for non-sorcery/instant. + */ + @Test + public void testNonSorceryOrInstant() { + addCard(Zone.BATTLEFIELD, playerA, anhelo); + addCard(Zone.BATTLEFIELD, playerA, mountain); + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker); + addCard(Zone.HAND, playerA, solRing); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, solRing); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, solRing, 1); + assertPermanentCount(playerA, aetherwindBasker, 1); + } + + /** + * Test that the instant side of an MDFC gains casualty + */ + @Test + public void testInstantSideMDFC() { + addCard(Zone.BATTLEFIELD, playerA, anhelo); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker); + addCard(Zone.HAND, playerA, flamescrollCelebrant); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, revelInSilence); + setChoice(playerA, "Yes"); // Cast with Casualty + setChoice(playerA, aetherwindBasker); + // Spell has no targets, so not prompted to change them + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, aetherwindBasker, 0); + } + + /** + * Test that the non-instant side of an MDFC (one which has an instant on the other side) does NOT gain casualty. + */ + @Test + public void testNonInstantSideMDFC() { + addCard(Zone.BATTLEFIELD, playerA, anhelo); + addCard(Zone.BATTLEFIELD, playerA, mountain, 2); + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker); + addCard(Zone.HAND, playerA, flamescrollCelebrant); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, flamescrollCelebrant); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, aetherwindBasker, 1); + assertPermanentCount(playerA, flamescrollCelebrant, 1); + } + + /** + * Test that it works for one you cast on someone else's turn. + */ + @Test + public void testOnNotOwnTurn() { + addCard(Zone.BATTLEFIELD, playerA, anhelo); + addCard(Zone.BATTLEFIELD, playerA, mountain, 2); + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker, 2); + addCard(Zone.HAND, playerA, lightningBolt, 2); + + setStrictChooseMode(true); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, playerB); + setChoice(playerA, "Yes"); // Cast with Casualty + setChoice(playerA, aetherwindBasker); + setChoice(playerA, "No"); // Don't change targets + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, playerB); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertLife(playerB, 20 - 2*3 - 3); + assertPermanentCount(playerA, aetherwindBasker, 1); + } + + /** + * Test that a card which already has Casualty will gain a second instance of Casualty and thus let you sacrifice twice in order to get 2 copies. + */ + @Test + public void testGainsSecondCasualty() { + addCard(Zone.BATTLEFIELD, playerA, anhelo); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, aetherwindBasker, 2); + addCard(Zone.HAND, playerA, aLittleChat); + addCard(Zone.LIBRARY, playerA, "Desert", 6); + + setStrictChooseMode(true); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aLittleChat); + setChoice(playerA, "Yes"); // First instance of casualty + setChoice(playerA, "Yes"); // Second instance of casualty + setChoice(playerA, aetherwindBasker); + setChoice(playerA, aetherwindBasker); + setChoice(playerA, "When you do"); // Chose which of the two copies to put on the stack first + addTarget(playerA, "Desert"); + addTarget(playerA, "Desert"); + addTarget(playerA, "Desert"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertHandCount(playerA, 3); + assertPermanentCount(playerA, aetherwindBasker, 0); + } + + /** + * Test that it does not work for opponents on your turn or on their. + */ + @Test + public void testOpponentCasts() { + addCard(Zone.BATTLEFIELD, playerA, anhelo); + addCard(Zone.BATTLEFIELD, playerB, mountain, 2); + addCard(Zone.BATTLEFIELD, playerB, aetherwindBasker, 2); + addCard(Zone.HAND, playerB, lightningBolt, 2); + + setStrictChooseMode(true); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, lightningBolt, playerA); + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, lightningBolt, playerA); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertLife(playerA, 20 - 3 - 3); + assertPermanentCount(playerB, aetherwindBasker, 2); + } +} \ No newline at end of file diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/BessSoulNourisherTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/BessSoulNourisherTest.java new file mode 100644 index 0000000000..7469fbdee8 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/BessSoulNourisherTest.java @@ -0,0 +1,83 @@ +package org.mage.test.cards.single.ncc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * {@link mage.cards.b.BessSoulNourisher Bess, Soul Nourisher} + *

+ * Whenever one or more other creatures with base power and toughness 1/1 enter the battlefield under your control, + * put a +1/+1 counter on Bess, Soul Nourisher. + *

+ * Whenever Bess attacks, each other creature you control with base power and toughness 1/1 gets +X/+X until end of turn, + * where X is the number of +1/+1 counters on Bess. + * + * @author Alex-Vasile + */ +public class BessSoulNourisherTest extends CardTestPlayerBase { + + // {1}{G}{W} + private static final String bessSoulNourisher = "Bess, Soul Nourisher"; + // {3}{W} + // Create three 1/1 white Soldier creature tokens. + private static final String captainsCall = "Captain's Call"; + + /** + * Test that it only triggers once for a group entering + */ + @Test + public void testEntersGroup() { + addCard(Zone.BATTLEFIELD, playerA, bessSoulNourisher); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 4); + addCard(Zone.HAND, playerA, captainsCall); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, captainsCall); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertPermanentCount(playerA, "Soldier Token", 3); + assertCounterCount(playerA, bessSoulNourisher, CounterType.P1P1, 1); // Should only get one +1/+1 since all soldiers tokens enter at once + } + + /** + * Test that the boosting corectly affects only creatures with BASE power and toughness of 1/1. + * Vodalian is a 1/1 so it should be buffed + * Artic merfolk is a 1/1 buffed to a 2/2, but it still has BASE PT of 1/1, so it should be buffed. + * Banewhip Punisher base 2/2 with a -1/-1 counter on it, so a 1/1, but its BASE is still 2/2, so it should not be buffed. + */ + @Test + public void testBoost() { + // 1/1 + // Other Merfolk you control get +1/+1. + String vodalianHexcatcher = "Vodalian Hexcatcher"; + // 1/1 + String arcticMerfolk = "Arctic Merfolk"; + // 2/2 + // When Banewhip Punisher enters the battlefield, you may put a -1/-1 counter on target creature. + String banewhipPunisher = "Banewhip Punisher"; + + addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 5); + addCard(Zone.BATTLEFIELD, playerA, bessSoulNourisher); + addCard(Zone.BATTLEFIELD, playerA, arcticMerfolk); + addCard(Zone.HAND, playerA, banewhipPunisher); + addCard(Zone.HAND, playerA, vodalianHexcatcher); + + setStrictChooseMode(true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, banewhipPunisher, true); + setChoice(playerA, "Yes"); + addTarget(playerA, banewhipPunisher); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, vodalianHexcatcher); + + attack(1, playerA, bessSoulNourisher); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertCounterCount(bessSoulNourisher, CounterType.P1P1, 1); + assertLife(playerB, 20 - 2); + assertPowerToughness(playerA, vodalianHexcatcher, 2, 2); // 1/1 + 1/1 from Bess + assertPowerToughness(playerA, arcticMerfolk, 3, 3); // 1/1 + (1/1 from Bess) + (1/1 Vodalian) + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/MariTheKillingQuillTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/MariTheKillingQuillTest.java new file mode 100644 index 0000000000..fd4fb45c3c --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/MariTheKillingQuillTest.java @@ -0,0 +1,82 @@ +package org.mage.test.cards.single.ncc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * {@link mage.cards.m.MariTheKillingQuill Mari, the Killing Quill} {1}{B}{B} + *

+ * Whenever a creature an opponent controls dies, exile it with a hit counter on it. + *

+ * Assassins, Mercenaries, and Rogues you control have deathtouch and + * "Whenever this creature deals combat damage to a player, you may remove a hit counter from a card that player owns in exile. + * If you do, draw a card and create two Treasure tokens." + * + * @author Alex-Vasile + */ +public class MariTheKillingQuillTest extends CardTestPlayerBase { + + private static final String mari = "Mari, the Killing Quill"; + private static final String lightningBolt = "Lightning Bolt"; + // Sliver with no ability 1/1 + private static final String sliver = "Metallic Sliver"; + // Changeling 1/1 + private static final String automation = "Universal Automaton"; + + /** + * Test that an opponent's creature will get exiled with a hit counter. + * And that one of ours does not. + */ + @Test + public void testExiledWithCounter() { + addCard(Zone.HAND, playerA, lightningBolt, 2); + addCard(Zone.BATTLEFIELD, playerA, mari); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerA, automation); + + addCard(Zone.BATTLEFIELD, playerB, sliver); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, sliver); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, automation); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertExileCount(playerA, 0); + assertGraveyardCount(playerA, automation, 1); + + assertExileCount(playerB, sliver, 1); + assertCounterOnExiledCardCount(sliver, CounterType.HIT, 1); + } + + /** + * Test that an opponent's creature will get exiled with a hit counter. + * And that one of ours does not. + */ + @Test + public void testDrawAndTreasure() { + addCard(Zone.HAND, playerA, lightningBolt); + addCard(Zone.BATTLEFIELD, playerA, mari); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, automation); + addCard(Zone.BATTLEFIELD, playerB, sliver); + + setStrictChooseMode(true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, sliver); + attack(1, playerA, automation, playerB); + setChoice(playerA, "Yes"); + setChoice(playerA, sliver); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertHandCount(playerA, 1); + assertPermanentCount(playerA, "Treasure Token", 2); + + assertExileCount(playerB, sliver, 1); + assertCounterOnExiledCardCount(sliver, CounterType.HIT, 0); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ResourcefulDefenseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ResourcefulDefenseTest.java new file mode 100644 index 0000000000..78ee55569c --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ResourcefulDefenseTest.java @@ -0,0 +1,159 @@ +package org.mage.test.cards.single.ncc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * {@link mage.cards.r.ResourcefulDefense Resourceful Defense} {2}{W} + *

+ * Whenever a permanent you control leaves the battlefield, if it had counters on it, + * put those counters on target permanent you control. + *

+ * {4}{W}: Move any number of counters from target permanent you control to another target permanent you control. + * + * @author Alex-Vasile + */ +public class ResourcefulDefenseTest extends CardTestPlayerBase { + private static final String resourcefulDefense = "Resourceful Defense"; + // Vivid Creek enters the battlefield tapped with two charge counters on it. + private static final String vividCreek = "Vivid Creek"; + private static final String everflowingChalice = "Everflowing Chalice"; + // Steelbane Hydra enters the battlefield with X +1/+1 counters on it. + private static final String steelbaneHydra = "Steelbane Hydra"; // {X}{G}{G} + private static final String lightningBolt = "Lightning Bolt"; + + /** + * Move counters from a creature that died. + */ + @Test + public void testMoveWhenDied() { + addCard(Zone.BATTLEFIELD, playerA, "Archway Commons", 9); + addCard(Zone.BATTLEFIELD, playerA, resourcefulDefense); + addCard(Zone.BATTLEFIELD, playerA, everflowingChalice); + addCard(Zone.HAND, playerA, steelbaneHydra); + addCard(Zone.HAND, playerA, lightningBolt); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, steelbaneHydra); + setChoice(playerA, "X=1"); + castSpell(1, PhaseStep.BEGIN_COMBAT, playerA, lightningBolt, steelbaneHydra); + addTarget(playerA, everflowingChalice); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertCounterCount(everflowingChalice, CounterType.P1P1, 1); + } + + /** + * Move all of one counter from one permanent to another when the source only has one coutner type. + */ + @Test + public void testMoveAllSingleCounters() { + addCard(Zone.BATTLEFIELD, playerA, "Archway Commons", 5); + addCard(Zone.BATTLEFIELD, playerA, resourcefulDefense); + addCard(Zone.BATTLEFIELD, playerA, vividCreek); + addCard(Zone.BATTLEFIELD, playerA, everflowingChalice); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{W}: "); + addTarget(playerA, vividCreek); + addTarget(playerA, everflowingChalice); + setChoiceAmount(playerA, 2); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertCounterCount(vividCreek, CounterType.CHARGE, 0); + assertCounterCount(everflowingChalice, CounterType.CHARGE, 2); + } + + /** + * Move some of one counter from one permanent to another when the source only has one coutner type. + */ + @Test + public void testSomeAllSingleCounters() { + addCard(Zone.BATTLEFIELD, playerA, "Archway Commons", 5); + addCard(Zone.BATTLEFIELD, playerA, resourcefulDefense); + addCard(Zone.BATTLEFIELD, playerA, vividCreek); + addCard(Zone.BATTLEFIELD, playerA, everflowingChalice); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{W}: "); + addTarget(playerA, vividCreek); + addTarget(playerA, everflowingChalice); + setChoiceAmount(playerA, 1); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertCounterCount(vividCreek, CounterType.CHARGE, 1); + assertCounterCount(everflowingChalice, CounterType.CHARGE, 1); + } + + /** + * Move multiple counter types from one permanent to another. + * + * Also tests that when a creature without counters dies that you won't be prompted. + * The hydra has no counters after the second activation and will die because toughtness==0, but we aren't prompted + * for targets when it dies. + */ + @Test + public void testMoveAllMultipleCounters() { + addCard(Zone.BATTLEFIELD, playerA, "Archway Commons", 8); + addCard(Zone.BATTLEFIELD, playerA, resourcefulDefense); + addCard(Zone.BATTLEFIELD, playerA, vividCreek); + addCard(Zone.HAND, playerA, steelbaneHydra); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, steelbaneHydra); + setChoice(playerA, "X=1"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{W}: "); + addTarget(playerA, vividCreek); + addTarget(playerA, steelbaneHydra); + setChoiceAmount(playerA, 2); + + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{W}: "); + addTarget(playerA, steelbaneHydra); + addTarget(playerA, vividCreek); + setChoiceAmount(playerA, 2); + setChoiceAmount(playerA, 1); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + assertCounterCount(vividCreek, CounterType.CHARGE, 2); + assertCounterCount(vividCreek, CounterType.P1P1, 1); + assertGraveyardCount(playerA, steelbaneHydra, 1); + } + + /** + * Move multiple counter types from a creature that died. + */ + @Test + public void testMoveMultipleWhenDied() { + addCard(Zone.BATTLEFIELD, playerA, "Archway Commons", 9); + addCard(Zone.BATTLEFIELD, playerA, resourcefulDefense); + addCard(Zone.BATTLEFIELD, playerA, everflowingChalice); + addCard(Zone.BATTLEFIELD, playerA, vividCreek); + addCard(Zone.HAND, playerA, steelbaneHydra); + addCard(Zone.HAND, playerA, lightningBolt); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, steelbaneHydra); + setChoice(playerA, "X=1"); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{W}: "); + addTarget(playerA, vividCreek); + addTarget(playerA, steelbaneHydra); + setChoiceAmount(playerA, 2); + + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, steelbaneHydra); + addTarget(playerA, everflowingChalice); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertCounterCount(vividCreek, CounterType.CHARGE, 0); + assertCounterCount(everflowingChalice, CounterType.CHARGE, 2); + assertCounterCount(everflowingChalice, CounterType.P1P1, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ShieldBrokerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ShieldBrokerTest.java new file mode 100644 index 0000000000..921f0a81fa --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ShieldBrokerTest.java @@ -0,0 +1,73 @@ +package org.mage.test.cards.single.ncc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +/** + * {@link mage.cards.s.ShieldBroker Shield Broker} + * {3}{U}{U} + * Creature — Cephalid Advisor + * When Shield Broker enters the battlefield, put a shield counter on target noncommander creature you don’t control. + * You gain control of that creature for as long as it has a shield counter on it. + * (If it would be dealt damage or destroyed, remove a shield counter from it instead.) + */ +public class ShieldBrokerTest extends CardTestCommander4Players { + + private static final String shieldBroker = "Shield Broker"; + private static final String rograkh = "Rograkh, Son of Rohgahh"; + private static final String lightningBolt = "Lightning Bolt"; + + /** + * Test that it works for non-commander creature. + */ + @Test + public void testNonCommander() { + addCard(Zone.BATTLEFIELD, playerD, rograkh); // {0} + + addCard(Zone.HAND, playerA, shieldBroker); // {3}{U}{U} + addCard(Zone.HAND, playerA, lightningBolt); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shieldBroker); + + setStopAt(1, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertCounterCount(rograkh, CounterType.SHIELD, 1); + assertPermanentCount(playerA, rograkh, 1); + + playLand(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Mountain"); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, lightningBolt, rograkh); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertCounterCount(rograkh, CounterType.SHIELD, 0); + assertPermanentCount(playerA, rograkh, 0); + assertPermanentCount(playerD, rograkh, 1); + } + + /** + * Test that it does not work for commander creature. + */ + @Test + public void testCommander() { + addCard(Zone.COMMAND, playerD, rograkh); // {0} + + addCard(Zone.HAND, playerA, shieldBroker); // {3}{U}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, rograkh); + + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, shieldBroker); + + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertCounterCount(rograkh, CounterType.SHIELD, 0); + assertPermanentCount(playerA, rograkh, 0); + assertPermanentCount(playerD, rograkh, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SinisterConciergeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SinisterConciergeTest.java new file mode 100644 index 0000000000..c63d0be9ec --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SinisterConciergeTest.java @@ -0,0 +1,75 @@ +package org.mage.test.cards.single.ncc; + +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.SuspendAbility; +import mage.constants.PhaseStep; +import mage.constants.TimingRule; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * {@link mage.cards.s.SinisterConcierge Sinister Concierge} + * 2/1 + * When Sinister Concierge dies, you may exile it and put three time counters on it. + * If you do, exile up to one target creature and put three time counters on it. + * Each card exiled this way that doesn’t have suspend gains suspend. + * @author Alex-Vasile + */ +public class SinisterConciergeTest extends CardTestPlayerBase { + + private static final String sinisterConcierge = "Sinister Concierge"; // 2/1 + private static final String bondedConstruct = "Bonded Construct"; // Simple 2/1 + private static final String lightningBolt = "Lightning Bolt"; // {R} + + + /** + * Test that both cards are exiled properly. + */ + @Test + public void testWorking() { + addCard(Zone.HAND, playerA, lightningBolt); + addCard(Zone.BATTLEFIELD, playerA, sinisterConcierge); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + + addCard(Zone.BATTLEFIELD, playerB, bondedConstruct); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, sinisterConcierge); + setChoice(playerA, "Yes"); + addTarget(playerA, bondedConstruct); + + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertExileCount(playerA, sinisterConcierge, 1); + assertCounterOnExiledCardCount(sinisterConcierge, CounterType.TIME, 3); + + assertExileCount(playerB, bondedConstruct, 1); + assertCounterOnExiledCardCount(bondedConstruct, CounterType.TIME, 3); + + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertExileCount(playerA, sinisterConcierge, 1); + assertCounterOnExiledCardCount(sinisterConcierge, CounterType.TIME, 1); + + assertExileCount(playerB, bondedConstruct, 1); + assertCounterOnExiledCardCount(bondedConstruct, CounterType.TIME, 1); + + setStopAt(6, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertExileCount(playerA, sinisterConcierge, 1); + assertExileCount(playerB, bondedConstruct, 0); + assertPermanentCount(playerB, bondedConstruct, 1); + + setStopAt(7, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertExileCount(playerA, sinisterConcierge, 0); + assertPermanentCount(playerA, sinisterConcierge, 1); + assertExileCount(playerB, bondedConstruct, 0); + assertPermanentCount(playerB, bondedConstruct, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SkywayRobberTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SkywayRobberTest.java new file mode 100644 index 0000000000..c4684aeae2 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/SkywayRobberTest.java @@ -0,0 +1,63 @@ +package org.mage.test.cards.single.ncc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * {@link mage.cards.s.SkywayRobber Skyway Robber} + *

+ * Escape—{3}{U}, Exile five other cards from your graveyard. + * (You may cast this card from your graveyard for its escape cost.) + *

+ * Skyway Robber escapes with + * “Whenever Skyway Robber deals combat damage to a player, + * you may cast an artifact, instant, or sorcery spell from among cards exiled with Skyway Robber + * without paying its mana cost.” + * @author Alex-Vasile + */ +public class SkywayRobberTest extends CardTestPlayerBase { + + private static final String skywayRobber = "Skyway Robber"; + + /** + * Check that you are not given the option to cast anything if there are no valid choices. + */ + @Test + public void testNoOption() { + addCard(Zone.GRAVEYARD, playerA, skywayRobber); + addCard(Zone.GRAVEYARD, playerA, "Mountain", 5); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + skywayRobber + " with Escape"); + + attack(3, playerA, skywayRobber); + + setStopAt(3, PhaseStep.END_COMBAT); + execute(); + assertExileCount(playerA, "Mountain", 5); + assertLife(playerB, 17); + } + + /** + * Check that the cast works. + */ + @Test + public void testCast() { + addCard(Zone.GRAVEYARD, playerA, skywayRobber); + addCard(Zone.GRAVEYARD, playerA, "Mountain", 4); + addCard(Zone.GRAVEYARD, playerA, "Sol Ring" ); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + skywayRobber + " with Escape"); + + attack(3, playerA, skywayRobber); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + assertExileCount(playerA, "Mountain", 4); + assertPermanentCount(playerA, "Sol Ring", 1); + assertLife(playerB, 17); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ThreefoldSignalTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ThreefoldSignalTest.java new file mode 100644 index 0000000000..f4bf7ef032 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/ThreefoldSignalTest.java @@ -0,0 +1,230 @@ +package org.mage.test.cards.single.ncc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * {@link mage.cards.t.ThreefoldSignal Threefold Signal} + *

+ * Each spell you cast that’s exactly three colors has replicate {3}. + * @author Alex-Vasile + */ +public class ThreefoldSignalTest extends CardTestPlayerBase { + + private static final String threefoldSignal = "Threefold Signal"; + // R + private static final String lightningBolt = "Lightning Bolt"; + // WUBRG + private static final String atogatog = "Atogatog"; + // WUB + private static final String esperSojourners = "Esper Sojourners"; + + /** + * Check that it works for three-colored spells + */ + @Test + public void testShouldWork() { + addCard(Zone.BATTLEFIELD, playerA, threefoldSignal); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2); + addCard(Zone.HAND, playerA, esperSojourners); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, esperSojourners); + setChoice(playerA, true); + setChoice(playerA, false); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, esperSojourners, 2); + } + + /** + * Check that it does not trigger for spells with less than three colors. + */ + @Test + public void testShouldNotWork1Color() { + addCard(Zone.BATTLEFIELD, playerA, threefoldSignal); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, lightningBolt); + + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, lightningBolt, playerB); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertLife(playerB, 17); + } + + /** + * Check that it does not trigger for spells with more than three colors. + */ + @Test + public void testShouldNotWork5Color() { + addCard(Zone.BATTLEFIELD, playerA, threefoldSignal); + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, "Forest"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, atogatog); + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, atogatog); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, atogatog, 1); + } + + /** + * Check that it does not trigger for spells opponents control. + */ + @Test + public void testShouldNotWorkOpponent() { + addCard(Zone.BATTLEFIELD, playerA, threefoldSignal); + addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + addCard(Zone.HAND, playerB, esperSojourners); + + setStrictChooseMode(true); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, esperSojourners); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerB, esperSojourners, 1); + } + + /** + * Check that casting one half of a split card doesn't trigger it even if the whole split card has 3 colors. + * Relevant ruling: + * 709.3a Only the chosen half is evaluated to see if it can be cast. + * Only that half is considered to be put onto the stack. + * 709.3b While on the stack, only the characteristics of the half being cast exist. + * The other half’s characteristics are treated as though they didn’t exist. + */ + @Test + public void oneHalfOfSplitCardDoesntTrigger() { + // {G}{U} / {4}{W}{U} + // Beck: Whenever a creature enters the battlefield this turn, you may draw a card. + // Call: Create four 1/1 white Bird creature tokens with flying. + // Fuse + String beckCall = "Beck // Call"; + addCard(Zone.HAND, playerA, beckCall); + + addCard(Zone.BATTLEFIELD, playerA, threefoldSignal); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4 + 3); // For generic costs and to have enough for replicate + + setStrictChooseMode(true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Call"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Bird Token", 4); + } + + /** + * Test that casting a split card with fuse triggers if both halves together have 3 colors + */ + @Test + public void fusedSplitCardTriggers() { + // {G}{U} / {4}{W}{U} + // Beck: Whenever a creature enters the battlefield this turn, you may draw a card. + // Call: Create four 1/1 white Bird creature tokens with flying. + // Fuse + String beckCall = "Beck // Call"; + addCard(Zone.HAND, playerA, beckCall); + + addCard(Zone.BATTLEFIELD, playerA, threefoldSignal); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4 + 3); // For generic costs and to have enough for replicate + + setStrictChooseMode(true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "fused Beck // Call"); + setChoice(playerA, true); // Pay replicate once + setChoice(playerA, false); // Don't pay replicate twice + + // Copy resolves, first Beck then call + setChoice(playerA, "Whenever", 3); // 4 triggers total, pick order for 3 and the 4th is auto-chosen + setChoice(playerA, true, 4); // Draw cards 4 times + + // Original resolves + // There will be 8 ETB triggers. 4 creatures enter but there are 2 instances of Beck that were cast + setChoice(playerA, "Whenever", 7); // 8 triggers total, pick order for 7 and the 8th is auto-chosen + setChoice(playerA, true, 8); // Draw cards 8 times + + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Bird Token", 4 + 4); + assertHandCount(playerA, 4 + (4+4)); + } + + /** + * Test that casting a split card with fuse triggers if both halves together have 3 colors + */ + @Test + public void fusedSplitCardTriggers2() { + // {3}{B}{G} / {R}{G} + // Flesh: Exile target creature card from a graveyard. + // Put X +1/+1 counters on target creature, where X is the power of the card you exiled. + // Blood: Target creature you control deals damage equal to its power to any target. + // Fuse + String fleshBlood = "Flesh // Blood"; + addCard(Zone.HAND, playerA, fleshBlood); + + addCard(Zone.BATTLEFIELD, playerA, threefoldSignal); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1+3 + 3); // For red + generic costs and to have enough for replicate + + // All are 2/2 + String lion = "Silvercoat Lion"; + addCard(Zone.BATTLEFIELD, playerA, lion); + String griffin = "Abbey Griffin"; + addCard(Zone.GRAVEYARD, playerA, griffin); // Exile with original cast + String centaur = "Accursed Centaur"; + addCard(Zone.GRAVEYARD, playerA, centaur); // Exile with copy + + setStrictChooseMode(true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "fused Flesh // Blood"); + setChoice(playerA, true); // Pay replicate once + setChoice(playerA, false); // Don't pay replicate twice + // Flesh + addTarget(playerA, griffin); + addTarget(playerA, lion); + // Blood + addTarget(playerA, lion); + addTarget(playerA, playerB); + // Copy of Flesh + setChoice(playerA, true); // Change the exile card from the Griffin + addTarget(playerA, centaur); + setChoice(playerA, false); // Don't change target from the lion + // Copy of Blood + setChoice(playerA, false); // Don't change target from lion + setChoice(playerA, false); // Don't change target from PlayerB + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertCounterCount(lion, CounterType.P1P1, 4); // 2 from the copy and two from the original cast + assertLife(playerB, 20 - (2+2) - (2+2+2)); + assertExileCount(playerA, griffin, 1); + assertExileCount(playerA, centaur, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/WeatheredSentinelsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/WeatheredSentinelsTest.java new file mode 100644 index 0000000000..89adef4376 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/WeatheredSentinelsTest.java @@ -0,0 +1,68 @@ +package org.mage.test.cards.single.ncc; + +import mage.abilities.keyword.IndestructibleAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * Weathered Sentinels + * {3} + * Artifact Creature — Wall + * Defender, vigilance, reach, trample + * Weathered Sentinels can attack players who attacked you during their last turn as though it didn’t have defender. + * Whenever Weathered Sentinels attacks, it gets +3/+3 and gains indestructible until end of turn. + */ +public class WeatheredSentinelsTest extends CardTestPlayerBase { + + private static final String weatheredSentinels = "Weathered Sentinels"; + // 1/1 Haste attacker + private static final String gingerBrute = "Gingerbrute"; + + /** + * Should not be able to attack a player who did not attack you on their last turn + */ + @Test + public void testCantAttackNonAttacker() { + addCard(Zone.BATTLEFIELD, playerA, weatheredSentinels); + + attack(1, playerA, weatheredSentinels); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + try { + execute(); + } catch (Throwable e) { + if (!e.getMessage().contains("Player PlayerA must have 0 actions but found 1")) { + Assert.fail("Should have had error about playerA not being able to attack, but got:\n" + e.getMessage()); + } + } + } + + /** + * Should be able to attack a player that attacked you on their last turn, and it should get +3/+3 and indestructible until end of turn. + */ + @Test + public void testCanAttackAttacker() { + addCard(Zone.BATTLEFIELD, playerA, weatheredSentinels); + addCard(Zone.BATTLEFIELD, playerB, gingerBrute); + + // Attack playerA + attack(2, playerB, gingerBrute); + + // Attack back + attack(3, playerA, weatheredSentinels); + + // Check that Weathered Sentinels has a +3/+3 and indestructible + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbility(playerA, weatheredSentinels, IndestructibleAbility.getInstance(), true); + assertPowerToughness(playerA, weatheredSentinels, 5, 8); + + // Check that Weathered Sentinels lost the abilities next turn + setStopAt(4, PhaseStep.PRECOMBAT_MAIN); + execute(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/utils/CardUtilTest.java b/Mage.Tests/src/test/java/org/mage/test/utils/CardUtilTest.java new file mode 100644 index 0000000000..e6f26a9888 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/utils/CardUtilTest.java @@ -0,0 +1,95 @@ +package org.mage.test.utils; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class CardUtilTest extends CardTestPlayerBase { + // Whenever you cast or copy an instant or sorcery spell, reveal the top card of your library. + // If it’s a nonland card, you may cast it by paying {1} rather than paying its mana cost. + // If it’s a land card, put it onto the battlefield. + private static final String jadzi = "Jadzi, Oracle of Arcavios"; + // Whenever you discard a nonland card, you may cast it from your graveyard. + private static final String oskar = "Oskar, Rubbish Reclaimer"; + // MDFC where the back side is a land "Akoum Teeth" + private static final String akoumWarrior = "Akoum Warrior"; // {5}{R} + // Discard your hand, then draw a card for each card you’ve discarded this turn. + private static final String changeOfFortune = "Change of Fortune"; // {3}{R} + // MDFC where both sides should be playable + private static final String birgi = "Birgi, God of Storytelling"; // {2}{R}, frontside of Harnfel + private static final String harnfel = "Harnfel, Horn of Bounty"; // {4}{R}, backside of Birgi + + /** + * Test that it will for trigger for discarding a MDFC but will only let you cast the nonland side. + */ + @Test + public void cantPlayLandSideOfMDFC() { + addCard(Zone.HAND, playerA, changeOfFortune); + addCard(Zone.HAND, playerA, akoumWarrior); + + addCard(Zone.BATTLEFIELD, playerA, oskar); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 10); + + skipInitShuffling(); + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, changeOfFortune); + setChoice(playerA, "Yes"); + + setStopAt(1, PhaseStep.DECLARE_ATTACKERS); + execute(); + assertPermanentCount(playerA, akoumWarrior, 1); + } + + /** + * Test that when both sides of a MDFC card match, we can choose either side. + */ + @Test + public void testFrontSideOfMDFC() { + addCard(Zone.HAND, playerA, changeOfFortune); + addCard(Zone.HAND, playerA, birgi, 2); + + addCard(Zone.BATTLEFIELD, playerA, oskar); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 12); + + skipInitShuffling(); + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, changeOfFortune); + setChoice(playerA, "Whenever you discard"); + setChoice(playerA, "Yes"); + setChoice(playerA, "Cast " + birgi); + setChoice(playerA, "Yes"); + setChoice(playerA, "Cast " + harnfel); + + setStopAt(1, PhaseStep.DECLARE_ATTACKERS); + execute(); + assertPermanentCount(playerA, birgi, 1); + assertPermanentCount(playerA, harnfel, 1); + } + + /** + * Test that with Jadzi, you are able to play the nonland side of a MDFC, and that the alternative cost works properly. + */ + @Test + public void testJadziPlayingLandAndCast() { + addCard(Zone.BATTLEFIELD, playerA, jadzi); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1+1+1); + addCard(Zone.HAND, playerA, "Lightning Bolt", 2); + addCard(Zone.LIBRARY, playerA, "Cragcrown Pathway"); + addCard(Zone.LIBRARY, playerA, akoumWarrior); + + skipInitShuffling(); + setStrictChooseMode(true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + setChoice(playerA, "Yes"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + + setStopAt(1, PhaseStep.DECLARE_ATTACKERS); + execute(); + assertPermanentCount(playerA, akoumWarrior, 1); + assertPermanentCount(playerA, "Cragcrown Pathway", 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/EnchantedPlayerAttackedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/EnchantedPlayerAttackedTriggeredAbility.java index 05bb099c98..3636505cf5 100644 --- a/Mage/src/main/java/mage/abilities/common/EnchantedPlayerAttackedTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/EnchantedPlayerAttackedTriggeredAbility.java @@ -8,6 +8,9 @@ import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; +import java.util.Set; +import java.util.UUID; + /** * @author LevelX2 */ @@ -31,10 +34,22 @@ public class EnchantedPlayerAttackedTriggeredAbility extends TriggeredAbilityImp public boolean checkTrigger(GameEvent event, Game game) { Permanent enchantment = game.getPermanentOrLKIBattlefield(getSourceId()); Player controller = game.getPlayer(getControllerId()); - if (controller != null && enchantment != null) { - return game.getCombat().getPlayerDefenders(game, false).contains(enchantment.getAttachedTo()); + Player attacker = game.getPlayer(game.getCombat().getAttackingPlayerId()); + if (controller == null || attacker == null || enchantment == null) { + return false; } - return false; + + Player enchantedPlayer = game.getPlayer(enchantment.getAttachedTo()); + if (enchantedPlayer == null) { + return false; + } + + Set opponentIds = game.getOpponents(controller.getId()); + if (!opponentIds.contains(attacker.getId()) || !opponentIds.contains(enchantedPlayer.getId())) { + return false; + } + + return game.getCombat().getPlayerDefenders(game, false).contains(enchantment.getAttachedTo()); } @Override diff --git a/Mage/src/main/java/mage/abilities/common/EntersBattlefieldOneOrMoreTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/EntersBattlefieldOneOrMoreTriggeredAbility.java new file mode 100644 index 0000000000..4a432b888c --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/EntersBattlefieldOneOrMoreTriggeredAbility.java @@ -0,0 +1,95 @@ +package mage.abilities.common; + +import mage.MageItem; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeGroupEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; + +import java.util.Objects; +import java.util.stream.Stream; + +/** + * "Whenever one or more {filter} enter the battlefield under {target controller} control, + * + * @author Alex-Vasile + */ +public class EntersBattlefieldOneOrMoreTriggeredAbility extends TriggeredAbilityImpl { + + private final FilterPermanent filterPermanent; + private final TargetController targetController; + + public EntersBattlefieldOneOrMoreTriggeredAbility(Effect effect, FilterPermanent filter, TargetController targetController) { + super(Zone.BATTLEFIELD, effect); + this.filterPermanent = filter; + this.targetController = targetController; + setTriggerPhrase(generateTriggerPhrase()); + } + + private EntersBattlefieldOneOrMoreTriggeredAbility(final EntersBattlefieldOneOrMoreTriggeredAbility ability) { + super(ability); + this.filterPermanent = ability.filterPermanent; + this.targetController = ability.targetController; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE_GROUP; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + ZoneChangeGroupEvent zEvent = (ZoneChangeGroupEvent) event; + Player controller = game.getPlayer(this.controllerId); + if (zEvent.getToZone() != Zone.BATTLEFIELD || controller == null) { + return false; + } + + switch (this.targetController) { + case YOU: + if (!controller.getId().equals(zEvent.getPlayerId())) { + return false; + } + break; + case OPPONENT: + if (!controller.hasOpponent(zEvent.getPlayerId(), game)) { + return false; + } + break; + } + + return Stream.concat( + zEvent.getTokens().stream(), + zEvent.getCards().stream() + .map(MageItem::getId) + .map(game::getPermanent) + .filter(Objects::nonNull) + ).anyMatch(permanent -> filterPermanent.match(permanent, this.controllerId, this, game)); + } + + @Override + public EntersBattlefieldOneOrMoreTriggeredAbility copy() { + return new EntersBattlefieldOneOrMoreTriggeredAbility(this); + } + + private String generateTriggerPhrase() { + StringBuilder sb = new StringBuilder("Whenever one or more " + this.filterPermanent.getMessage() + " enter the battlefield under "); + switch (targetController) { + case YOU: + sb.append("your control, "); + break; + case OPPONENT: + sb.append("an opponent's control, "); + break; + default: + throw new UnsupportedOperationException(); + } + return sb.toString(); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/EscapesWithAbility.java b/Mage/src/main/java/mage/abilities/common/EscapesWithAbility.java index 1c4e0b751f..9e6270d542 100644 --- a/Mage/src/main/java/mage/abilities/common/EscapesWithAbility.java +++ b/Mage/src/main/java/mage/abilities/common/EscapesWithAbility.java @@ -3,8 +3,11 @@ package mage.abilities.common; import mage.abilities.Ability; import mage.abilities.DelayedTriggeredAbility; import mage.abilities.SpellAbility; +import mage.abilities.TriggeredAbility; +import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.EntersBattlefieldEffect; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; import mage.abilities.keyword.EscapeAbility; import mage.constants.AbilityType; import mage.constants.Outcome; @@ -23,22 +26,22 @@ import java.util.UUID; public class EscapesWithAbility extends EntersBattlefieldAbility { private final int counters; - private final DelayedTriggeredAbility delayedTriggeredAbility; + private final TriggeredAbility triggeredAbility; public EscapesWithAbility(int counters) { this(counters, null); } - public EscapesWithAbility(int counters, DelayedTriggeredAbility delayedTriggeredAbility) { - super(new EscapesWithEffect(counters, delayedTriggeredAbility), false); + public EscapesWithAbility(int counters, TriggeredAbility triggeredAbility) { + super(new EscapesWithEffect(counters, triggeredAbility), false); this.counters = counters; - this.delayedTriggeredAbility = delayedTriggeredAbility; + this.triggeredAbility = triggeredAbility; } private EscapesWithAbility(final EscapesWithAbility ability) { super(ability); this.counters = ability.counters; - this.delayedTriggeredAbility = ability.delayedTriggeredAbility; + this.triggeredAbility = ability.triggeredAbility; } @Override @@ -48,27 +51,42 @@ public class EscapesWithAbility extends EntersBattlefieldAbility { @Override public String getRule() { - return "{this} escapes with " + CardUtil.numberToText(counters, "a") - + " +1/+1 counter" + (counters > 1 ? 's' : "") + " on it." - + (this.delayedTriggeredAbility != null ? " " + this.delayedTriggeredAbility.getRule() : ""); + StringBuilder sb = new StringBuilder("{this} escapes with "); + if (counters > 0) { + sb.append(CardUtil.numberToText(counters, "a")); + sb.append(" +1/+1 counter"); + sb.append((counters > 1 ? 's' : "")); + sb.append(" on it."); + } + + if (triggeredAbility == null) { + // Do nothing + } else if (triggeredAbility instanceof DelayedTriggeredAbility) { + sb.append(this.triggeredAbility.getRule()); + } else { + sb.append("\""); + sb.append(this.triggeredAbility.getRule()); + sb.append("\""); + } + return sb.toString(); } } class EscapesWithEffect extends OneShotEffect { private final int counter; - private final DelayedTriggeredAbility delayedTriggeredAbility; + private final TriggeredAbility triggeredAbility; - EscapesWithEffect(int counter, DelayedTriggeredAbility delayedTriggeredAbility) { + EscapesWithEffect(int counter, TriggeredAbility triggeredAbility) { super(Outcome.BoostCreature); this.counter = counter; - this.delayedTriggeredAbility = delayedTriggeredAbility; + this.triggeredAbility = triggeredAbility; } private EscapesWithEffect(final EscapesWithEffect effect) { super(effect); this.counter = effect.counter; - this.delayedTriggeredAbility = (effect.delayedTriggeredAbility == null ? null : effect.delayedTriggeredAbility.copy()); + this.triggeredAbility = (effect.triggeredAbility == null ? null : effect.triggeredAbility.copy()); } @Override @@ -80,16 +98,26 @@ class EscapesWithEffect extends OneShotEffect { if (permanent == null) { return false; } + SpellAbility spellAbility = (SpellAbility) getValue(EntersBattlefieldEffect.SOURCE_CAST_SPELL_ABILITY); if (!(spellAbility instanceof EscapeAbility) || !spellAbility.getSourceId().equals(source.getSourceId()) || permanent.getZoneChangeCounter(game) != spellAbility.getSourceObjectZoneChangeCounter()) { return false; } - List appliedEffects = (ArrayList) this.getValue("appliedEffects"); - permanent.addCounters(CounterType.P1P1.createInstance(counter), source.getControllerId(), source, game, appliedEffects); - if (this.delayedTriggeredAbility != null) { - game.addDelayedTriggeredAbility(this.delayedTriggeredAbility, source); + + if (counter > 0) { + List appliedEffects = (ArrayList) this.getValue("appliedEffects"); + permanent.addCounters(CounterType.P1P1.createInstance(counter), source.getControllerId(), source, game, appliedEffects); + } + + if (this.triggeredAbility != null) { + if (triggeredAbility instanceof DelayedTriggeredAbility) { + game.addDelayedTriggeredAbility((DelayedTriggeredAbility) this.triggeredAbility, source); + } else { + ContinuousEffect gainsAbilityEffect = new GainAbilitySourceEffect(triggeredAbility); + game.addEffect(gainsAbilityEffect, source); + } } return true; } diff --git a/Mage/src/main/java/mage/abilities/common/SpellCastControllerTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/SpellCastControllerTriggeredAbility.java index 9bfa87005e..f8ae1fcfd9 100644 --- a/Mage/src/main/java/mage/abilities/common/SpellCastControllerTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/SpellCastControllerTriggeredAbility.java @@ -20,8 +20,10 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl { // The source SPELL that triggered the ability will be set as target to effect protected boolean rememberSource; - // Use it if you want remember CARD instead spell + // Use it if you want to remember CARD instead spell protected boolean rememberSourceAsCard; + // Trigger only for spells cast from this zone. Default is from any zone. + private Zone fromZone = Zone.ALL; public SpellCastControllerTriggeredAbility(Effect effect, boolean optional) { this(Zone.BATTLEFIELD, effect, StaticFilters.FILTER_SPELL_A, optional, false); @@ -31,6 +33,11 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl { this(effect, filter, optional, false); } + public SpellCastControllerTriggeredAbility(Effect effect, FilterSpell filter, boolean optional, Zone fromZone) { + this(effect, filter, optional, false); + this.fromZone = fromZone; + } + public SpellCastControllerTriggeredAbility(Effect effect, FilterSpell filter, boolean optional, String rule) { this(effect, filter, optional, false); this.rule = rule; @@ -49,7 +56,7 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl { this.filter = filter; this.rememberSource = rememberSource; this.rememberSourceAsCard = rememberSourceAsCard; - setTriggerPhrase("Whenever you cast " + filter.getMessage() + ", "); + setTriggerPhrase("Whenever you cast " + filter.getMessage() + (fromZone != Zone.ALL ? "from your " + fromZone.toString().toLowerCase() : "") + ", "); } public SpellCastControllerTriggeredAbility(final SpellCastControllerTriggeredAbility ability) { @@ -58,6 +65,7 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl { this.rule = ability.rule; this.rememberSource = ability.rememberSource; this.rememberSourceAsCard = ability.rememberSourceAsCard; + this.fromZone = ability.fromZone; } @Override @@ -67,22 +75,20 @@ public class SpellCastControllerTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (event.getPlayerId().equals(this.getControllerId())) { - Spell spell = game.getStack().getSpell(event.getTargetId()); - if (filter.match(spell, getControllerId(), this, game)) { - this.getEffects().setValue("spellCast", spell); - if (rememberSource) { - if (rememberSourceAsCard) { - this.getEffects().setTargetPointer(new FixedTarget(spell.getCard().getId(), game)); - } else { - this.getEffects().setTargetPointer(new FixedTarget(spell.getId(), game)); - } - - } - return true; - } + if (!event.getPlayerId().equals(this.getControllerId())) { + return false; } - return false; + Spell spell = game.getStack().getSpell(event.getTargetId()); + if (spell == null + || !filter.match(spell, getControllerId(), this, game) + || !(fromZone == Zone.ALL || fromZone == spell.getFromZone())) { + return false; + } + this.getEffects().setValue("spellCast", spell); + if (rememberSource) { + this.getEffects().setTargetPointer(new FixedTarget(rememberSourceAsCard ? spell.getCard().getId() : spell.getId(), game)); + } + return true; } @Override diff --git a/Mage/src/main/java/mage/abilities/common/SpellCastOpponentTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/SpellCastOpponentTriggeredAbility.java index ddc9a681dc..260a1935dc 100644 --- a/Mage/src/main/java/mage/abilities/common/SpellCastOpponentTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/SpellCastOpponentTriggeredAbility.java @@ -18,37 +18,60 @@ public class SpellCastOpponentTriggeredAbility extends TriggeredAbilityImpl { protected FilterSpell filter; protected SetTargetPointer setTargetPointer; + private final boolean onlyFromNonHand; public SpellCastOpponentTriggeredAbility(Effect effect, boolean optional) { - this(effect, StaticFilters.FILTER_SPELL_A, optional); + this(effect, optional, false); + } + + public SpellCastOpponentTriggeredAbility(Effect effect, boolean optional, boolean onlyFromNonHand) { + this(effect, StaticFilters.FILTER_SPELL_A, optional, onlyFromNonHand); } public SpellCastOpponentTriggeredAbility(Effect effect, FilterSpell filter, boolean optional) { - this(Zone.BATTLEFIELD, effect, filter, optional); + this(effect, filter, optional, false); + } + + public SpellCastOpponentTriggeredAbility(Effect effect, FilterSpell filter, boolean optional, boolean onlyFromNonHand) { + this(Zone.BATTLEFIELD, effect, filter, optional, onlyFromNonHand); } public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional) { - this(zone, effect, filter, optional, SetTargetPointer.NONE); + this(zone, effect, filter, optional, false); + } + + public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, boolean onlyFromNonHand) { + this(zone, effect, filter, optional, SetTargetPointer.NONE, onlyFromNonHand); + } + + public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, SetTargetPointer setTargetPointer) { + this(zone, effect, filter, optional, setTargetPointer, false); } /** - * @param zone - * @param effect - * @param filter - * @param optional - * @param setTargetPointer Supported: SPELL, PLAYER + * @param zone The zone in which the source permanent has to be in for the ability to trigger + * @param effect The effect to apply if condition is met + * @param filter Filter for matching the spell cast + * @param optional Whether the player can choose to apply the effect + * @param onlyFromNonHand Whether to trigger only when spells are cast from not the hand + * @param setTargetPointer Supported: SPELL, PLAYER */ - public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, SetTargetPointer setTargetPointer) { + public SpellCastOpponentTriggeredAbility(Zone zone, Effect effect, FilterSpell filter, boolean optional, SetTargetPointer setTargetPointer, boolean onlyFromNonHand) { super(zone, effect, optional); this.filter = filter; this.setTargetPointer = setTargetPointer; - setTriggerPhrase("Whenever an opponent casts " + filter.getMessage() + ", "); + this.onlyFromNonHand = onlyFromNonHand; + setTriggerPhrase("Whenever an opponent casts " + + filter.getMessage() + + (onlyFromNonHand ? " from anywhere other than their hand" : "") + + ", "); } public SpellCastOpponentTriggeredAbility(final SpellCastOpponentTriggeredAbility ability) { super(ability); this.filter = ability.filter; this.setTargetPointer = ability.setTargetPointer; + this.onlyFromNonHand = ability.onlyFromNonHand; } @Override @@ -65,6 +88,11 @@ public class SpellCastOpponentTriggeredAbility extends TriggeredAbilityImpl { if (!filter.match(spell, getControllerId(), this, game)) { return false; } + + if (onlyFromNonHand && spell.getFromZone() == Zone.HAND) { + return false; + } + getEffects().setValue("spellCast", spell); switch (setTargetPointer) { case NONE: diff --git a/Mage/src/main/java/mage/abilities/common/TargetOfOpponentsSpellOrAbilityTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/TargetOfOpponentsSpellOrAbilityTriggeredAbility.java new file mode 100644 index 0000000000..22732036cc --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/TargetOfOpponentsSpellOrAbilityTriggeredAbility.java @@ -0,0 +1,91 @@ +package mage.abilities.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +/** + * "Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls," + * AND + * "Whenever you become the target of a spell or ability an opponent controls," + * + * @author Alex-Vasile + */ +public class TargetOfOpponentsSpellOrAbilityTriggeredAbility extends TriggeredAbilityImpl { + + private Boolean onlyController = Boolean.FALSE; + + public TargetOfOpponentsSpellOrAbilityTriggeredAbility(Effect effect) { + this(effect, false); + } + + // NOTE: Using Boolean instead of boolean in order to have a second constructor with (Effect, "boolean") signature + public TargetOfOpponentsSpellOrAbilityTriggeredAbility(Effect effect, Boolean onlyController) { + this(effect, false, onlyController); + } + + public TargetOfOpponentsSpellOrAbilityTriggeredAbility(Effect effect, boolean optional) { + this(effect, optional, Boolean.FALSE); + } + + public TargetOfOpponentsSpellOrAbilityTriggeredAbility(Effect effect, boolean optional, Boolean onlyController) { + super(Zone.BATTLEFIELD, effect, optional); + this.onlyController = onlyController; + if (this.onlyController) { + setTriggerPhrase("Whenever you become the target of a spell or ability an opponent controls, "); + } else { + setTriggerPhrase("Whenever you or a permanent you control becomes the target of a spell or ability an opponent controls, "); + } + } + + private TargetOfOpponentsSpellOrAbilityTriggeredAbility(final TargetOfOpponentsSpellOrAbilityTriggeredAbility ability) { + super(ability); + this.onlyController = ability.onlyController; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.TARGETED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + Player controller = game.getPlayer(this.getControllerId()); + Player targetter = game.getPlayer(event.getPlayerId()); + if (controller == null || targetter == null || controller.getId().equals(targetter.getId())) { + return false; + } + + // Check if player was targeted + if (controller.getId().equals(event.getTargetId())) { + // Add target for effects which need it (e.g. the counter effect from AmuletOfSafekeeping) + this.getEffects().setTargetPointer(new FixedTarget(event.getSourceId(), game)); + return true; + } + + // If only the controller is + if (this.onlyController) { + return false; + } + + // Check if permanent was targeted + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); + if (permanent == null || !controller.getId().equals(permanent.getControllerId())) { + return false; + } + + // Add target for effects which need it (e.g. the counter effect from AmuletOfSafekeeping) + this.getEffects().setTargetPointer(new FixedTarget(event.getSourceId(), game)); + return false; + } + + @Override + public TargetOfOpponentsSpellOrAbilityTriggeredAbility copy() { + return new TargetOfOpponentsSpellOrAbilityTriggeredAbility(this); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/SourceHasCountersCondition.java b/Mage/src/main/java/mage/abilities/condition/common/SourceHasCountersCondition.java new file mode 100644 index 0000000000..a04071f0e2 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/SourceHasCountersCondition.java @@ -0,0 +1,25 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.counters.Counter; +import mage.game.Game; +import mage.game.permanent.Permanent; + +public enum SourceHasCountersCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getSourceId()); + if (permanent == null) { + return false; + } + return permanent + .getCounters(game) + .values() + .stream() + .mapToInt(Counter::getCount) + .anyMatch(x -> x > 0); + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/common/RemoveCounterCost.java b/Mage/src/main/java/mage/abilities/costs/common/RemoveCounterCost.java index 036f83ac51..269ee539d0 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/RemoveCounterCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/RemoveCounterCost.java @@ -3,6 +3,7 @@ package mage.abilities.costs.common; import mage.abilities.Ability; import mage.abilities.costs.Cost; import mage.abilities.costs.CostImpl; +import mage.cards.Card; import mage.choices.Choice; import mage.choices.ChoiceImpl; import mage.constants.Outcome; @@ -11,6 +12,9 @@ import mage.counters.CounterType; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import mage.target.Target; +import mage.target.TargetCard; +import mage.target.TargetObject; import mage.target.TargetPermanent; import mage.util.CardUtil; @@ -23,19 +27,19 @@ import java.util.UUID; */ public class RemoveCounterCost extends CostImpl { - protected final TargetPermanent target; + protected final Target target; private final CounterType counterTypeToRemove; protected final int countersToRemove; - public RemoveCounterCost(TargetPermanent target) { + public RemoveCounterCost(Target target) { this(target, null); } - public RemoveCounterCost(TargetPermanent target, CounterType counterTypeToRemove) { + public RemoveCounterCost(Target target, CounterType counterTypeToRemove) { this(target, counterTypeToRemove, 1); } - public RemoveCounterCost(TargetPermanent target, CounterType counterTypeToRemove, int countersToRemove) { + public RemoveCounterCost(Target target, CounterType counterTypeToRemove, int countersToRemove) { this.target = target; this.counterTypeToRemove = counterTypeToRemove; this.countersToRemove = countersToRemove; @@ -50,70 +54,94 @@ public class RemoveCounterCost extends CostImpl { this.counterTypeToRemove = cost.counterTypeToRemove; } + // TODO: TayamLuminousEnigmaCost can be simplified @Override public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { paid = false; int countersRemoved = 0; Player controller = game.getPlayer(controllerId); - if (controller != null) { - if (countersToRemove == 0) { // Can happen when used for X costs where X = 0; - return paid = true; - } - target.clearChosen(); - if (target.choose(Outcome.UnboostCreature, controllerId, source.getSourceId(), source, game)) { - for (UUID targetId : target.getTargets()) { - Permanent permanent = game.getPermanent(targetId); - if (permanent != null) { - if (!permanent.getCounters(game).isEmpty() && (counterTypeToRemove == null || permanent.getCounters(game).containsKey(counterTypeToRemove))) { - String counterName = null; + if (controller == null) { + return false; + } + if (countersToRemove == 0) { // Can happen when used for X costs where X = 0; + paid = true; + return paid; + } + target.clearChosen(); - if (counterTypeToRemove != null) { - counterName = counterTypeToRemove.getName(); - } else if (permanent.getCounters(game).size() > 1 && counterTypeToRemove == null) { - Choice choice = new ChoiceImpl(true); - Set choices = new HashSet<>(); - for (Counter counter : permanent.getCounters(game).values()) { - if (permanent.getCounters(game).getCount(counter.getName()) > 0) { - choices.add(counter.getName()); - } - } - choice.setChoices(choices); - choice.setMessage("Choose a counter to remove from " + permanent.getLogName()); - if (!controller.choose(Outcome.UnboostCreature, choice, game)) { - return false; - } - counterName = choice.getChoice(); - } else { - for (Counter counter : permanent.getCounters(game).values()) { - if (counter.getCount() > 0) { - counterName = counter.getName(); - } - } - } - if (counterName != null && !counterName.isEmpty()) { - int countersLeft = countersToRemove - countersRemoved; - int countersOnPermanent = permanent.getCounters(game).getCount(counterName); - int numberOfCountersSelected = 1; - if (countersLeft > 1 && countersOnPermanent > 1) { - numberOfCountersSelected = controller.getAmount(1, Math.min(countersLeft, countersOnPermanent), - new StringBuilder("Remove how many counters from ").append(permanent.getIdName()).toString(), game); - } - permanent.removeCounters(counterName, numberOfCountersSelected, source, game); - countersRemoved += numberOfCountersSelected; - if (!game.isSimulation()) { - game.informPlayers(new StringBuilder(controller.getLogName()) - .append(" removes ").append(numberOfCountersSelected == 1 ? "a" : numberOfCountersSelected).append(' ') - .append(counterName).append(numberOfCountersSelected == 1 ? " counter from " : " counters from ") - .append(permanent.getName()).toString()); - } - if (countersRemoved == countersToRemove) { - this.paid = true; - break; - } - } - } + Outcome outcome; + if (target instanceof TargetPermanent) { + outcome = Outcome.UnboostCreature; + } else if (target instanceof TargetCard) { // For Mari, the Killing Quill + outcome = Outcome.Neutral; + } else { + throw new RuntimeException( + "Wrong target type provided for RemoveCounterCost. Provided " + target.getClass() + ". " + + "From ability " + ability); + } + + if (!target.choose(outcome, controllerId, source.getSourceId(), source, game)) { + return paid; + } + for (UUID targetId : target.getTargets()) { + Card targetObject; + if (target instanceof TargetPermanent) { + targetObject = game.getPermanent(targetId); + } else { // For Mari, the Killing Quill + targetObject = game.getCard(targetId); + } + + if (targetObject == null + || targetObject.getCounters(game).isEmpty() + || !(counterTypeToRemove == null || targetObject.getCounters(game).containsKey(counterTypeToRemove))) { + continue; + } + String counterName = null; + + if (counterTypeToRemove != null) { // Counter type specified + counterName = counterTypeToRemove.getName(); + } else if (targetObject.getCounters(game).size() == 1) { // Only one counter of creature + for (Counter counter : targetObject.getCounters(game).values()) { + if (counter.getCount() > 0) { + counterName = counter.getName(); } } + } else { // Multiple counters, player much choose which type to remove from + Choice choice = new ChoiceImpl(true); + Set choices = new HashSet<>(); + for (Counter counter : targetObject.getCounters(game).values()) { + if (targetObject.getCounters(game).getCount(counter.getName()) > 0) { + choices.add(counter.getName()); + } + } + choice.setChoices(choices); + choice.setMessage("Choose a counter to remove from " + targetObject.getLogName()); + if (!controller.choose(Outcome.UnboostCreature, choice, game)) { + return false; + } + counterName = choice.getChoice(); + } + + if (counterName != null && !counterName.isEmpty()) { + int countersLeft = countersToRemove - countersRemoved; + int countersOnPermanent = targetObject.getCounters(game).getCount(counterName); + int numberOfCountersSelected = 1; + if (countersLeft > 1 && countersOnPermanent > 1) { + numberOfCountersSelected = controller.getAmount(1, Math.min(countersLeft, countersOnPermanent), + "Remove how many counters from " + targetObject.getIdName(), game); + } + targetObject.removeCounters(counterName, numberOfCountersSelected, source, game); + countersRemoved += numberOfCountersSelected; + if (!game.isSimulation()) { + game.informPlayers(controller.getLogName() + + " removes " + (numberOfCountersSelected == 1 ? "a" : numberOfCountersSelected) + ' ' + + counterName + (numberOfCountersSelected == 1 ? " counter from " : " counters from ") + + targetObject.getName()); + } + if (countersRemoved == countersToRemove) { + this.paid = true; + break; + } } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/PutCardFromOneOfTwoZonesOntoBattlefieldEffect.java b/Mage/src/main/java/mage/abilities/effects/common/PutCardFromOneOfTwoZonesOntoBattlefieldEffect.java new file mode 100644 index 0000000000..1449797097 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/PutCardFromOneOfTwoZonesOntoBattlefieldEffect.java @@ -0,0 +1,164 @@ +package mage.abilities.effects.common; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CommanderCardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureCard; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInCommandZone; +import mage.target.common.TargetCardInGraveyard; +import mage.target.common.TargetCardInHand; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; + +/** + * "Put a {filter} card from {zone 1} or {zone 2} onto the battlefield. + * + * @author TheElk801, Alex-Vasile + */ +public class PutCardFromOneOfTwoZonesOntoBattlefieldEffect extends OneShotEffect { + + private final FilterCard filterCard; + private final boolean tapped; + private final Effect effectToApplyOnPermanent; + private final Zone zone1; + private final Zone zone2; + + public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard) { + this(filterCard, false); + } + + public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard, boolean tapped) { + this(filterCard, tapped, null); + } + + /** + * + * @param filterCard Filter used to filter which cards are valid choices. (no default) + * @param tapped If the permanent should enter the battlefield tapped (default is False) + * @param effectToApplyOnPermanent An effect to apply to the permanent after it enters (default null) + * See "Swift Warkite" or "Nissa of Shadowed Boughs". + * @param zone1 The first zone to pick from (default of HAND) + * @param zone2 The second zone to pick from (defualt of GRAVEYARD) + */ + public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard, boolean tapped, Effect effectToApplyOnPermanent, Zone zone1, Zone zone2) { + super(filterCard instanceof FilterCreatureCard ? Outcome.PutCreatureInPlay : Outcome.PutCardInPlay); + this.filterCard = filterCard; + this.tapped = tapped; + this.effectToApplyOnPermanent = effectToApplyOnPermanent; + this.zone1 = zone1; + this.zone2 = zone2; + } + + public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard, boolean tapped, Effect effectToApplyOnPermanent) { + this(filterCard, tapped, effectToApplyOnPermanent, Zone.HAND, Zone.GRAVEYARD); + } + + public PutCardFromOneOfTwoZonesOntoBattlefieldEffect(FilterCard filterCard, Zone zone1, Zone zone2) { + this(filterCard, false, null, zone1, zone2); + } + + private PutCardFromOneOfTwoZonesOntoBattlefieldEffect(final PutCardFromOneOfTwoZonesOntoBattlefieldEffect effect) { + super(effect); + this.filterCard = effect.filterCard; + this.tapped = effect.tapped; + this.zone1 = effect.zone1; + this.zone2 = effect.zone2; + if (effect.effectToApplyOnPermanent != null) { + this.effectToApplyOnPermanent = effect.effectToApplyOnPermanent.copy(); + } else { + this.effectToApplyOnPermanent = null; + } + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + + Cards cardsInZone1 = getCardsFromZone(game, controller, zone1); + Cards cardsInZone2 = getCardsFromZone(game, controller, zone2); + + boolean cardsAvailableInZone1 = cardsInZone1.count(filterCard, game) > 0; + boolean cardsAvailableInZone2 = cardsInZone2.count(filterCard, game) > 0; + if (!cardsAvailableInZone1 && !cardsAvailableInZone2) { + return false; + } + + boolean choose1stZone; + if (cardsAvailableInZone1 && cardsAvailableInZone2) { + choose1stZone = controller.chooseUse(outcome, "Where do you want to chose the card from?", + null, zone1.name(), zone2.name(), source, game); + } else { + choose1stZone = cardsAvailableInZone1; + } + + Zone zone = choose1stZone ? zone1 : zone2; + Cards cards = choose1stZone ? cardsInZone1 : cardsInZone2; + TargetCard targetCard; + + switch (zone) { + case HAND: + targetCard = new TargetCardInHand(filterCard); + break; + case GRAVEYARD: + targetCard = new TargetCardInGraveyard(filterCard); + break; + case COMMAND: + targetCard = new TargetCardInCommandZone(filterCard); + break; + default: + return false; + } + controller.choose(outcome, cards, targetCard, game); + Card card = game.getCard(targetCard.getFirstTarget()); + if (card == null || !controller.moveCards(card, Zone.BATTLEFIELD, source, game, tapped, false, false, null)) { + return false; + } + + if (effectToApplyOnPermanent != null) { + effectToApplyOnPermanent.setTargetPointer(new FixedTarget(card.getId())); + effectToApplyOnPermanent.apply(game, source); + } + return true; + } + + private static Cards getCardsFromZone(Game game, Player player, Zone zone) { + switch (zone) { + case HAND: + return player.getHand(); + case COMMAND: + return new CardsImpl(game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY)); + case GRAVEYARD: + return player.getGraveyard(); + default: + return new CardsImpl(); + } + } + + @Override + public PutCardFromOneOfTwoZonesOntoBattlefieldEffect copy() { + return new PutCardFromOneOfTwoZonesOntoBattlefieldEffect(this); + } + + @Override + public String getText(Mode mode) { + return "you may put " + CardUtil.addArticle(this.filterCard.getMessage()) + + " from your hand or graveyard onto the battlefield" + + (this.tapped ? " tapped" : "") + + (effectToApplyOnPermanent == null ? "" : ". " + effectToApplyOnPermanent.getText(mode)); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/ReturnToBattlefieldAttachedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ReturnToBattlefieldAttachedEffect.java new file mode 100644 index 0000000000..67e6396c1c --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/ReturnToBattlefieldAttachedEffect.java @@ -0,0 +1,49 @@ +package mage.abilities.effects.common; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; + +/** + * Used for "Return {this} to the battlefield attached to that creature at the beginning of the next end step" abilities. + * E.g.g Gift of Immortality and Next of Kin. + * + * @author LevelX2, Alex-Vasile + */ +public class ReturnToBattlefieldAttachedEffect extends OneShotEffect { + + public ReturnToBattlefieldAttachedEffect() { + super(Outcome.PutCardInPlay); + staticText = "Return {this} to the battlefield attached to that creature at the beginning of the next end step"; + } + + public ReturnToBattlefieldAttachedEffect(final ReturnToBattlefieldAttachedEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Permanent creature = game.getPermanent(getTargetPointer().getFirst(game, source)); + Card aura = game.getCard(source.getSourceId()); + if (controller == null + || creature == null + || aura == null || game.getState().getZone(aura.getId()) != Zone.GRAVEYARD) { + return false; + } + + game.getState().setValue("attachTo:" + aura.getId(), creature); + controller.moveCards(aura, Zone.BATTLEFIELD, source, game); + return creature.addAttachment(aura.getId(), source, game); + } + + @Override + public ReturnToBattlefieldAttachedEffect copy() { + return new ReturnToBattlefieldAttachedEffect(this); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/EachSpellYouCastHasReplicateEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/EachSpellYouCastHasReplicateEffect.java new file mode 100644 index 0000000000..b7f45342e5 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/EachSpellYouCastHasReplicateEffect.java @@ -0,0 +1,94 @@ +package mage.abilities.effects.common.continuous; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.costs.Cost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.keyword.ReplicateAbility; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; +import mage.filter.FilterSpell; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; +import mage.game.stack.StackObject; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + + +/** + * @author LevelX2, Alex-Vasile + */ +public class EachSpellYouCastHasReplicateEffect extends ContinuousEffectImpl { + + private final FilterSpell filter; + private final Cost fixedNewCost; + private final Map replicateAbilities = new HashMap<>(); + + public EachSpellYouCastHasReplicateEffect(FilterSpell filter) { + this(filter, null); + } + + /** + * + * @param filter Filter used for filtering spells + * @param fixedNewCost Fixed new cost to pay as the replication cost + */ + public EachSpellYouCastHasReplicateEffect(FilterSpell filter, Cost fixedNewCost) { + super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.filter = filter; + this.fixedNewCost = fixedNewCost; + this.staticText = "Each " + this.filter.getMessage() + " you cast has replicate" + + (this.fixedNewCost == null ? ". The replicate cost is equal to its mana cost" : ' ' + this.fixedNewCost.getText()) + + ". (When you cast it, copy it for each time you paid its replicate cost. You may choose new targets for the copies.)"; + } + + private EachSpellYouCastHasReplicateEffect(final EachSpellYouCastHasReplicateEffect effect) { + super(effect); + this.filter = effect.filter; + this.fixedNewCost = effect.fixedNewCost; + this.replicateAbilities.putAll(effect.replicateAbilities); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getSourceId()); + if (permanent == null + || !permanent.isControlledBy(source.getControllerId())) { // Verify that the controller of the permanent is the one who cast the spell + return false; + } + + boolean applied = false; + + for (StackObject stackObject : game.getStack()) { + if (!(stackObject instanceof Spell) + || stackObject.isCopy() + || !stackObject.isControlledBy(source.getControllerId()) + || (fixedNewCost == null && stackObject.getManaCost().isEmpty())) { // If the spell has no mana cost, it cannot be played by this ability unless an fixed alternative cost (e.g. such as from Threefold Signal) is specified. + continue; + } + Spell spell = (Spell) stackObject; + if (filter.match(stackObject, game)) { + Cost cost = fixedNewCost != null ? fixedNewCost.copy() : spell.getSpellAbility().getManaCosts().copy(); + ReplicateAbility replicateAbility = replicateAbilities.computeIfAbsent(spell.getId(), k -> new ReplicateAbility(cost)); + game.getState().addOtherAbility(spell.getCard(), replicateAbility, false); // Do not copy because paid and # of activations state is handled in the baility + applied = true; + } + } + if (game.getStack().isEmpty()) { + replicateAbilities.clear(); + } + + return applied; + } + + @Override + public EachSpellYouCastHasReplicateEffect copy() { + return new EachSpellYouCastHasReplicateEffect(this); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainControlTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainControlTargetEffect.java index ad66fc7eee..e9d60a5ac6 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainControlTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainControlTargetEffect.java @@ -3,6 +3,7 @@ package mage.abilities.effects.common.continuous; import mage.abilities.Ability; import mage.abilities.ActivatedAbility; import mage.abilities.Mode; +import mage.abilities.condition.Condition; import mage.abilities.effects.ContinuousEffectImpl; import mage.constants.Duration; import mage.constants.Layer; @@ -24,6 +25,7 @@ public class GainControlTargetEffect extends ContinuousEffectImpl { protected UUID controllingPlayerId; private boolean fixedControl; private boolean firstControlChange = true; + private final Condition condition; public GainControlTargetEffect(Duration duration) { this(duration, false, null); @@ -48,15 +50,21 @@ public class GainControlTargetEffect extends ContinuousEffectImpl { } public GainControlTargetEffect(Duration duration, boolean fixedControl, UUID controllingPlayerId) { + this(duration, fixedControl, controllingPlayerId, null); + } + + public GainControlTargetEffect(Duration duration, boolean fixedControl, UUID controllingPlayerId, Condition condition) { super(duration, Layer.ControlChangingEffects_2, SubLayer.NA, Outcome.GainControl); this.controllingPlayerId = controllingPlayerId; this.fixedControl = fixedControl; + this.condition = condition; } public GainControlTargetEffect(final GainControlTargetEffect effect) { super(effect); this.controllingPlayerId = effect.controllingPlayerId; this.fixedControl = effect.fixedControl; + this.condition = effect.condition; } @Override @@ -106,6 +114,9 @@ public class GainControlTargetEffect extends ContinuousEffectImpl { // This does not handle correctly multiple targets at once discard(); } + if (condition != null && !condition.apply(game, source)) { + discard(); + } } // no valid target exists and the controller is no longer in the game, effect can be discarded if (!oneTargetStillExists || !controller.isInGame()) { diff --git a/Mage/src/main/java/mage/abilities/effects/common/counter/AddCounterChoiceSourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/counter/AddCounterChoiceSourceEffect.java index 0d539e7639..615cac8598 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/counter/AddCounterChoiceSourceEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/counter/AddCounterChoiceSourceEffect.java @@ -2,34 +2,55 @@ package mage.abilities.effects.common.counter; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; import mage.constants.Outcome; import mage.counters.Counter; import mage.counters.CounterType; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; +import mage.util.CardUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.Locale; +import java.util.stream.Collectors; /** * @author TheElk801 */ public class AddCounterChoiceSourceEffect extends OneShotEffect { - private final CounterType counterType1; - private final CounterType counterType2; + private final List counterTypes; - public AddCounterChoiceSourceEffect(CounterType counterType1, CounterType counterType2) { + public AddCounterChoiceSourceEffect(CounterType ... counterTypes) { super(Outcome.Benefit); - this.counterType1 = counterType1; - this.counterType2 = counterType2; - staticText = "with your choice of a " + counterType1 + " counter or a " + counterType2 + " counter on it"; + this.counterTypes = Arrays.stream(counterTypes).collect(Collectors.toList()); + this.createStaticText(); } private AddCounterChoiceSourceEffect(final AddCounterChoiceSourceEffect effect) { super(effect); - this.counterType1 = effect.counterType1; - this.counterType2 = effect.counterType2; + this.counterTypes = new ArrayList<>(effect.counterTypes); + } + + private void createStaticText() { + switch (this.counterTypes.size()) { + case 0: + case 1: + throw new IllegalArgumentException("AddCounterChoiceSourceEffect should be called with at least 2 " + + "counter types, it was called with: " + this.counterTypes); + case 2: + this.staticText = "with your choice of a " + this.counterTypes.get(0) + + " counter or a " + this.counterTypes.get(1) + " counter on it"; + break; + default: + List strings = this.counterTypes.stream().map(CounterType::toString).collect(Collectors.toList()); + this.staticText = CardUtil.concatWithOr(strings); + break; + } } @Override @@ -39,19 +60,30 @@ public class AddCounterChoiceSourceEffect extends OneShotEffect { if (player == null || permanent == null) { return false; } - Counter counter; - if (player.chooseUse( - Outcome.Neutral, "Choose " + counterType1 + " or " + counterType2, null, - cap(counterType1.getName()), cap(counterType2.getName()), source, game - )) { - counter = counterType1.createInstance(); - } else { - counter = counterType2.createInstance(); + + Choice counterChoice = new ChoiceImpl(); + counterChoice.setMessage("Choose counter type"); + counterChoice.setChoices( + this.counterTypes + .stream() + .map(counterType -> AddCounterChoiceSourceEffect.capitalize(counterType.getName())) + .collect(Collectors.toSet()) + ); + + if (!player.choose(Outcome.Neutral, counterChoice, game)) { + return false; } + + CounterType counterChosen = CounterType.findByName(counterChoice.getChoice().toLowerCase(Locale.ENGLISH)); + if (counterChosen == null || !this.counterTypes.contains(counterChosen)) { + return false; + } + Counter counter = counterChosen.createInstance(); + return permanent.addCounters(counter, source.getControllerId(), source, game); } - private static final String cap(String string) { + private static String capitalize(String string) { return string != null ? string.substring(0, 1).toUpperCase(Locale.ENGLISH) + string.substring(1) : null; } diff --git a/Mage/src/main/java/mage/abilities/keyword/CasualtyAbility.java b/Mage/src/main/java/mage/abilities/keyword/CasualtyAbility.java index 39fba38fa3..a69ade91a3 100644 --- a/Mage/src/main/java/mage/abilities/keyword/CasualtyAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/CasualtyAbility.java @@ -1,87 +1,95 @@ package mage.abilities.keyword; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; import mage.abilities.common.delayed.ReflexiveTriggeredAbility; -import mage.abilities.costs.Cost; +import mage.abilities.costs.*; import mage.abilities.costs.common.SacrificeTargetCost; import mage.abilities.effects.common.CopySourceSpellEffect; -import mage.abilities.effects.common.InfoEffect; -import mage.cards.Card; import mage.constants.ComparisonType; +import mage.constants.Outcome; import mage.constants.Zone; import mage.filter.common.FilterControlledCreaturePermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.mageobject.PowerPredicate; import mage.game.Game; +import mage.players.Player; import mage.target.common.TargetControlledPermanent; -import java.util.UUID; - /** - * @author TheElk801 + * @author TheElk801, Alex-Vasile */ -public class CasualtyAbility extends StaticAbility { +public class CasualtyAbility extends StaticAbility implements OptionalAdditionalSourceCosts { - public CasualtyAbility(Card card, int number) { - super(Zone.ALL, new InfoEffect( - "Casualty " + number + " (As you cast this spell, " + - "you may sacrifice a creature with power " + number + - " or greater. When you do, copy this spell.)" - )); - card.getSpellAbility().addCost(new CasualtyCost(number)); + private static final String keywordText = "Casualty"; + private final String promptString; + + protected OptionalAdditionalCost additionalCost; + + private static TargetControlledPermanent makeFilter(int number) { + FilterControlledPermanent filter = new FilterControlledCreaturePermanent( + "creature with power " + number + " or greater" + ); + filter.add(new PowerPredicate(ComparisonType.MORE_THAN, number - 1)); + return new TargetControlledPermanent(1, 1, filter, true); + } + + public CasualtyAbility(int number) { + super(Zone.STACK, null); + String reminderText = "As you cast this spell, you may sacrifice a creature with power " + + number + " or greater. When you do, copy this spell."; + this.promptString = "Sacrifice a creature with power " + number + " or greater?"; + this.additionalCost = new OptionalAdditionalCostImpl(keywordText, reminderText, new SacrificeTargetCost(makeFilter(number))); + this.additionalCost.setRepeatable(false); this.setRuleAtTheTop(true); } private CasualtyAbility(final CasualtyAbility ability) { super(ability); + this.additionalCost = ability.additionalCost; + this.promptString = ability.promptString; + } + + public void resetCasualty() { + if (additionalCost != null) { + additionalCost.reset(); + } } @Override public CasualtyAbility copy() { return new CasualtyAbility(this); } -} - -class CasualtyCost extends SacrificeTargetCost { - - CasualtyCost(int number) { - super(new TargetControlledPermanent(0, 1, makeFilter(number), true)); - this.text = ""; - } - - private CasualtyCost(final CasualtyCost cost) { - super(cost); - } @Override - public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { - if (!super.pay(ability, game, source, controllerId, noMana, costToPay)) { - return false; + public void addOptionalAdditionalCosts(Ability ability, Game game) { + if (!(ability instanceof SpellAbility)) { + return; } - if (!getPermanents().isEmpty()) { - game.fireReflexiveTriggeredAbility(new ReflexiveTriggeredAbility( - new CopySourceSpellEffect(), false, "when you do, copy this spell" - ), source); + + Player player = game.getPlayer(ability.getControllerId()); + if (player == null) { + return; } - return true; + + this.resetCasualty(); + boolean canPay = additionalCost.canPay(ability, this, ability.getControllerId(), game); + if (!canPay || !player.chooseUse(Outcome.Sacrifice, promptString, ability, game)) { + return; + } + + additionalCost.activate(); + for (Cost cost : ((Costs) additionalCost)) { + ability.getCosts().add(cost.copy()); + } + game.fireReflexiveTriggeredAbility(new ReflexiveTriggeredAbility( + new CopySourceSpellEffect(), false, "when you do, copy this spell" + ), ability); } @Override - public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return true; - } - - @Override - public CasualtyCost copy() { - return new CasualtyCost(this); - } - - private static FilterControlledPermanent makeFilter(int number) { - FilterControlledPermanent filter = new FilterControlledCreaturePermanent( - "creature with power " + number + " or greater" - ); - filter.add(new PowerPredicate(ComparisonType.MORE_THAN, number - 1)); - return filter; + public String getCastMessageSuffix() { + return additionalCost == null ? "" : additionalCost.getCastSuffixMessage(0); } } diff --git a/Mage/src/main/java/mage/abilities/keyword/ReplicateAbility.java b/Mage/src/main/java/mage/abilities/keyword/ReplicateAbility.java index 838048a291..9df741cb06 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ReplicateAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ReplicateAbility.java @@ -61,17 +61,11 @@ public class ReplicateAbility extends StaticAbility implements OptionalAdditiona @Override public boolean isActivated() { - if (additionalCost != null) { - return additionalCost.isActivated(); - } - return false; + return additionalCost != null && additionalCost.isActivated(); } public int getActivateCount() { - if (additionalCost != null) { - return additionalCost.getActivateCount(); - } - return 0; + return additionalCost == null ? 0 : additionalCost.getActivateCount(); } public void resetReplicate() { @@ -82,36 +76,38 @@ public class ReplicateAbility extends StaticAbility implements OptionalAdditiona @Override public void addOptionalAdditionalCosts(Ability ability, Game game) { - if (ability instanceof SpellAbility) { - Player player = game.getPlayer(ability.getControllerId()); - if (player != null) { - this.resetReplicate(); - boolean again = true; - while (player.canRespond() - && again) { - String times = ""; - if (additionalCost.isRepeatable()) { - int numActivations = additionalCost.getActivateCount(); - times = (numActivations + 1) + (numActivations == 0 ? " time " : " times "); - } + if (!(ability instanceof SpellAbility)) { + return; + } - // TODO: add AI support to find max number of possible activations (from available mana) - // canPay checks only single mana available, not total mana usage - if (additionalCost.canPay(ability, this, ability.getControllerId(), game) - && player.chooseUse(/*Outcome.Benefit*/Outcome.AIDontUseIt, - new StringBuilder("Pay ").append(times).append( - additionalCost.getText(false)).append(" ?").toString(), ability, game)) { - additionalCost.activate(); - for (Iterator it = ((Costs) additionalCost).iterator(); it.hasNext(); ) { - Cost cost = (Cost) it.next(); - if (cost instanceof ManaCostsImpl) { - ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); - } else { - ability.getCosts().add(cost.copy()); - } - } + Player player = game.getPlayer(ability.getControllerId()); + if (player == null) { + return; + } + + this.resetReplicate(); + boolean again = true; + while (player.canRespond() && again) { + String times = ""; + if (additionalCost.isRepeatable()) { + int numActivations = additionalCost.getActivateCount(); + times = (numActivations + 1) + (numActivations == 0 ? " time " : " times "); + } + String payPrompt = "Pay " + times + additionalCost.getText(false) + " ?"; + + // TODO: add AI support to find max number of possible activations (from available mana) + // canPay checks only single mana available, not total mana usage + boolean canPay = additionalCost.canPay(ability, this, ability.getControllerId(), game); + if (!canPay || !player.chooseUse(/*Outcome.Benefit*/Outcome.AIDontUseIt, payPrompt, ability, game)) { + again = false; + } else { + additionalCost.activate(); + for (Iterator it = ((Costs) additionalCost).iterator(); it.hasNext(); ) { + Cost cost = (Cost) it.next(); + if (cost instanceof ManaCostsImpl) { + ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); } else { - again = false; + ability.getCosts().add(cost.copy()); } } } @@ -120,29 +116,16 @@ public class ReplicateAbility extends StaticAbility implements OptionalAdditiona @Override public String getRule() { - StringBuilder sb = new StringBuilder(); - if (additionalCost != null) { - sb.append(additionalCost.getText(false)); - sb.append(' ').append(additionalCost.getReminderText()); - } - return sb.toString(); + return additionalCost == null ? "" : additionalCost.getText(false) + ' ' + additionalCost.getReminderText(); } @Override public String getCastMessageSuffix() { - if (additionalCost != null) { - return additionalCost.getCastSuffixMessage(0); - } else { - return ""; - } + return additionalCost == null ? "" : additionalCost.getCastSuffixMessage(0); } public String getReminderText() { - if (additionalCost != null) { - return additionalCost.getReminderText(); - } else { - return ""; - } + return additionalCost == null ? "" : additionalCost.getReminderText(); } } @@ -169,24 +152,26 @@ class ReplicateTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (event.getSourceId().equals(this.sourceId)) { - StackObject spell = game.getStack().getStackObject(this.sourceId); - if (spell instanceof Spell) { - Card card = ((Spell) spell).getCard(); - if (card != null) { - for (Ability ability : card.getAbilities(game)) { - if (ability instanceof ReplicateAbility) { - if (ability.isActivated()) { - for (Effect effect : this.getEffects()) { - effect.setValue("ReplicateSpell", spell); - effect.setValue("ReplicateCount", ((ReplicateAbility) ability).getActivateCount()); - } - return true; - } - } - } - } + if (!event.getSourceId().equals(this.sourceId)) { + return false; + } + StackObject spell = game.getStack().getStackObject(this.sourceId); + if (!(spell instanceof Spell)) { + return false; + } + Card card = ((Spell) spell).getCard(); + if (card == null) { + return false; + } + for (Ability ability : card.getAbilities(game)) { + if (!(ability instanceof ReplicateAbility) || !ability.isActivated()) { + continue; } + for (Effect effect : this.getEffects()) { + effect.setValue("ReplicateSpell", spell); + effect.setValue("ReplicateCount", ((ReplicateAbility) ability).getActivateCount()); + } + return true; } return false; } @@ -211,29 +196,26 @@ class ReplicateCopyEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Spell spell = (Spell) this.getValue("ReplicateSpell"); - int replicateCount = (Integer) this.getValue("ReplicateCount"); - if (spell != null - && replicateCount > 0) { - // reset replicate now so the copies don't report x times Replicate - Card card = game.getCard(spell.getSourceId()); - if (card != null) { - for (Ability ability : card.getAbilities(game)) { - if (ability instanceof ReplicateAbility) { - if (ability.isActivated()) { - ((ReplicateAbility) ability).resetReplicate(); - } - } - } - } - // create the copies - spell.createCopyOnStack(game, source, source.getControllerId(), true, replicateCount); - return true; - } - + Spell spell = (Spell) this.getValue("ReplicateSpell"); + int replicateCount = (Integer) this.getValue("ReplicateCount"); + if (controller == null || spell == null || replicateCount == 0) { + return false; } - return false; + + // reset replicate now so the copies don't report x times Replicate + Card card = game.getCard(spell.getSourceId()); + if (card == null) { + return false; + } + + for (Ability ability : card.getAbilities(game)) { + if ((ability instanceof ReplicateAbility) && ability.isActivated()) { + ((ReplicateAbility) ability).resetReplicate(); + } + } + // create the copies + spell.createCopyOnStack(game, source, source.getControllerId(), true, replicateCount); + return true; } @Override diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index cf853b3717..15e44c6996 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -850,6 +850,21 @@ public final class StaticFilters { FILTER_CREATURE_TOKEN.setLockedFilter(true); } + public static final FilterCreaturePermanent FILTER_CONTROLLED_CREATURE_NON_TOKEN = new FilterCreaturePermanent("a nontoken creature you control"); + + static { + FILTER_CONTROLLED_CREATURE_NON_TOKEN.add(TargetController.YOU.getControllerPredicate()); + FILTER_CONTROLLED_CREATURE_NON_TOKEN.add(TokenPredicate.FALSE); + FILTER_CONTROLLED_CREATURE_NON_TOKEN.setLockedFilter(true); + } + + public static final FilterCreaturePermanent FILTER_CREATURE_NON_TOKEN = new FilterCreaturePermanent("a nontoken creature"); + + static { + FILTER_CREATURE_NON_TOKEN.add(TokenPredicate.FALSE); + FILTER_CREATURE_NON_TOKEN.setLockedFilter(true); + } + public static final FilterControlledCreaturePermanent FILTER_A_CONTROLLED_CREATURE_P1P1 = new FilterControlledCreaturePermanent("a creature you control with a +1/+1 counter on it"); static { diff --git a/Mage/src/main/java/mage/filter/predicate/card/CardManaCostLessThanControlledLandCountPredicate.java b/Mage/src/main/java/mage/filter/predicate/card/CardManaCostLessThanControlledLandCountPredicate.java new file mode 100644 index 0000000000..4bf5317182 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/card/CardManaCostLessThanControlledLandCountPredicate.java @@ -0,0 +1,31 @@ +package mage.filter.predicate.card; + +import mage.cards.Card; +import mage.filter.StaticFilters; +import mage.filter.predicate.Predicate; +import mage.game.Game; + +/** + * @author Plopman, Alex-Vasile + */ +public class CardManaCostLessThanControlledLandCountPredicate implements Predicate { + + private static final String string = "card with mana value less than or equal to the number of lands you control"; + private static final CardManaCostLessThanControlledLandCountPredicate instance = new CardManaCostLessThanControlledLandCountPredicate(); + + private CardManaCostLessThanControlledLandCountPredicate() { } + + public static CardManaCostLessThanControlledLandCountPredicate getInstance() { + return instance; + } + + @Override + public boolean apply(Card input, Game game) { + return input.getManaValue() <= game.getBattlefield().getAllActivePermanents(StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND, input.getOwnerId(), game).size(); + } + + @Override + public String toString() { + return string; + } +} diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/BasePowerPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/BasePowerPredicate.java new file mode 100644 index 0000000000..b4bedec700 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/BasePowerPredicate.java @@ -0,0 +1,26 @@ +package mage.filter.predicate.mageobject; + +import mage.MageObject; +import mage.constants.ComparisonType; +import mage.filter.predicate.IntComparePredicate; + +/** + * @author Alex-Vasile + */ +public class BasePowerPredicate extends IntComparePredicate { + + public BasePowerPredicate(ComparisonType type, int value) { + super(type, value); + } + + @Override + protected int getInputValue(MageObject input) { + return input.getPower().getModifiedBaseValue(); + } + + @Override + public String toString() { + return "Base power" + super.toString(); + } +} + diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/BaseToughnessPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/BaseToughnessPredicate.java new file mode 100644 index 0000000000..5e44835d16 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/BaseToughnessPredicate.java @@ -0,0 +1,23 @@ +package mage.filter.predicate.mageobject; + +import mage.MageObject; +import mage.constants.ComparisonType; +import mage.filter.predicate.IntComparePredicate; + +public class BaseToughnessPredicate extends IntComparePredicate { + + public BaseToughnessPredicate(ComparisonType type, int value) { + super(type, value); + } + + @Override + protected int getInputValue(MageObject input) { + return input.getToughness().getModifiedBaseValue(); + } + + @Override + public String toString() { + return "Base toughness" + super.toString(); + } +} + diff --git a/Mage/src/main/java/mage/target/common/TargetCardInCommandZone.java b/Mage/src/main/java/mage/target/common/TargetCardInCommandZone.java new file mode 100644 index 0000000000..3976176fae --- /dev/null +++ b/Mage/src/main/java/mage/target/common/TargetCardInCommandZone.java @@ -0,0 +1,87 @@ +package mage.target.common; + +import mage.abilities.Ability; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CommanderCardType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.game.events.TargetEvent; +import mage.players.Player; +import mage.target.TargetCard; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * Based on TargetCardInHand + * @author Alex-Vasile + */ +public class TargetCardInCommandZone extends TargetCard { + + public TargetCardInCommandZone(FilterCard filter) { + super(1, 1, Zone.COMMAND, filter); + } + + public TargetCardInCommandZone(final TargetCardInCommandZone targetCardInCommandZone) { + super(targetCardInCommandZone); + } + + @Override + public TargetCardInCommandZone copy() { + return new TargetCardInCommandZone(this); + } + + @Override + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + Card card = game.getCard(id); + return game.getState().getZone(id) == Zone.COMMAND + && game.getState().getPlayersInRange(getTargetController() == null ? playerId : getTargetController(), game).contains(game.getOwnerId(id)) + && filter.match(card, game); + } + + @Override + public boolean canTarget(UUID id, Ability source, Game game) { + return this.canTarget(source.getControllerId(), id, source, game); + } + + @Override + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + Set possibleTargets = new HashSet<>(); + Player player = game.getPlayer(sourceControllerId); + if (player == null) { + return possibleTargets; + } + + Cards cards = new CardsImpl(game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY)); + for (Card card : cards.getCards(filter, sourceControllerId, source, game)) { + if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { + possibleTargets.add(card.getId()); + } + } + return possibleTargets; + } + + @Override + public boolean canChoose(UUID sourceControllerId, Ability source, Game game) { + Player player = game.getPlayer(sourceControllerId); + if (player == null) { + return false; + } + + int possibletargets = 0; + Cards cards = new CardsImpl(game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY)); + for (Card card : cards.getCards(filter, sourceControllerId, source, game)) { + if (source == null || source.getSourceId() == null || isNotTarget() || !game.replaceEvent(new TargetEvent(card, source.getSourceId(), sourceControllerId))) { + possibletargets++; + if (possibletargets >= this.minNumberOfTargets) { + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/target/common/TargetOpponent.java b/Mage/src/main/java/mage/target/common/TargetOpponent.java index b974b870a2..3fbe826de1 100644 --- a/Mage/src/main/java/mage/target/common/TargetOpponent.java +++ b/Mage/src/main/java/mage/target/common/TargetOpponent.java @@ -1,6 +1,7 @@ package mage.target.common; import mage.filter.FilterOpponent; +import mage.filter.FilterPlayer; import mage.target.TargetPlayer; /** @@ -16,7 +17,11 @@ public class TargetOpponent extends TargetPlayer { } public TargetOpponent(boolean notTarget) { - super(1, 1, notTarget, filter); + this(1, 1, notTarget); + } + + public TargetOpponent(int minNumTargets, int maxNumTargets, boolean notTarget) { + super(minNumTargets, maxNumTargets, notTarget, filter); } private TargetOpponent(final TargetOpponent target) { diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 8a3c8ea295..f2d20c9965 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -9,6 +9,8 @@ import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.SpellAbility; import mage.abilities.condition.Condition; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; import mage.abilities.costs.VariableCost; import mage.abilities.costs.mana.*; import mage.abilities.dynamicvalue.DynamicValue; @@ -61,7 +63,7 @@ public final class CardUtil { public static final List RULES_ERROR_INFO = ImmutableList.of("Exception occurred in rules generation"); - private static final String SOURCE_EXILE_ZONE_TEXT = "SourceExileZone"; + public static final String SOURCE_EXILE_ZONE_TEXT = "SourceExileZone"; static final String[] numberStrings = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty"}; @@ -1332,6 +1334,103 @@ public final class CardUtil { } } + public static void castSingle(Player player, Ability source, Game game, Card card) { + castSingle(player, source, game, card, null); + } + + public static void castSingle(Player player, Ability source, Game game, Card card, ManaCostsImpl manaCost) { + // handle split-cards + if (card instanceof SplitCard) { + SplitCardHalf leftHalfCard = ((SplitCard) card).getLeftHalfCard(); + SplitCardHalf rightHalfCard = ((SplitCard) card).getRightHalfCard(); + if (manaCost != null) { + // get additional cost if any + Costs additionalCostsLeft = leftHalfCard.getSpellAbility().getCosts(); + Costs additionalCostsRight = rightHalfCard.getSpellAbility().getCosts(); + // set alternative cost and any additional cost + player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsLeft); + player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsRight); + } + // allow the card to be cast + game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), Boolean.TRUE); + game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), Boolean.TRUE); + } + + // handle MDFC + if (card instanceof ModalDoubleFacesCard) { + ModalDoubleFacesCardHalf leftHalfCard = ((ModalDoubleFacesCard) card).getLeftHalfCard(); + ModalDoubleFacesCardHalf rightHalfCard = ((ModalDoubleFacesCard) card).getRightHalfCard(); + if (manaCost != null) { + // some MDFC cards are lands. IE: sea gate restoration + if (!leftHalfCard.isLand(game)) { + // get additional cost if any + Costs additionalCostsMDFCLeft = leftHalfCard.getSpellAbility().getCosts(); + // set alternative cost and any additional cost + player.setCastSourceIdWithAlternateMana(leftHalfCard.getId(), manaCost, additionalCostsMDFCLeft); + } + if (!rightHalfCard.isLand(game)) { + // get additional cost if any + Costs additionalCostsMDFCRight = rightHalfCard.getSpellAbility().getCosts(); + // set alternative cost and any additional cost + player.setCastSourceIdWithAlternateMana(rightHalfCard.getId(), manaCost, additionalCostsMDFCRight); + } + } + // allow the card to be cast + game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), Boolean.TRUE); + game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), Boolean.TRUE); + } + + // handle adventure cards + if (card instanceof AdventureCard) { + Card creatureCard = card.getMainCard(); + Card spellCard = ((AdventureCard) card).getSpellCard(); + if (manaCost != null) { + // get additional cost if any + Costs additionalCostsCreature = creatureCard.getSpellAbility().getCosts(); + Costs additionalCostsSpellCard = spellCard.getSpellAbility().getCosts(); + // set alternative cost and any additional cost + player.setCastSourceIdWithAlternateMana(creatureCard.getId(), manaCost, additionalCostsCreature); + player.setCastSourceIdWithAlternateMana(spellCard.getId(), manaCost, additionalCostsSpellCard); + } + // allow the card to be cast + game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), Boolean.TRUE); + game.getState().setValue("PlayFromNotOwnHandZone" + spellCard.getId(), Boolean.TRUE); + } + + // normal card + if (manaCost != null) { + // get additional cost if any + Costs additionalCostsNormalCard = card.getSpellAbility().getCosts(); + player.setCastSourceIdWithAlternateMana(card.getMainCard().getId(), manaCost, additionalCostsNormalCard); + } + + // cast it + player.cast(player.chooseAbilityForCast(card.getMainCard(), game, false), + game, false, new ApprovingObject(source, game)); + + // turn off effect after cast on every possible card-face + if (card instanceof SplitCard) { + SplitCardHalf leftHalfCard = ((SplitCard) card).getLeftHalfCard(); + SplitCardHalf rightHalfCard = ((SplitCard) card).getRightHalfCard(); + game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null); + game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null); + } + if (card instanceof ModalDoubleFacesCard) { + ModalDoubleFacesCardHalf leftHalfCard = ((ModalDoubleFacesCard) card).getLeftHalfCard(); + ModalDoubleFacesCardHalf rightHalfCard = ((ModalDoubleFacesCard) card).getRightHalfCard(); + game.getState().setValue("PlayFromNotOwnHandZone" + leftHalfCard.getId(), null); + game.getState().setValue("PlayFromNotOwnHandZone" + rightHalfCard.getId(), null); + } + if (card instanceof AdventureCard) { + Card creatureCard = card.getMainCard(); + Card spellCard = ((AdventureCard) card).getSpellCard(); + game.getState().setValue("PlayFromNotOwnHandZone" + creatureCard.getId(), null); + game.getState().setValue("PlayFromNotOwnHandZone" + spellCard.getId(), null); + } + // turn off effect on a normal card + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + } + /** * Pay life in effects * diff --git a/Mage/src/main/java/mage/watchers/common/CreatedTokenWatcher.java b/Mage/src/main/java/mage/watchers/common/CreatedTokenWatcher.java index 2e948ad8c3..e87b867c44 100644 --- a/Mage/src/main/java/mage/watchers/common/CreatedTokenWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/CreatedTokenWatcher.java @@ -1,8 +1,10 @@ package mage.watchers.common; +import mage.cards.Card; import mage.constants.WatcherScope; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.permanent.token.Token; import mage.util.CardUtil; import mage.watchers.Watcher; @@ -15,7 +17,9 @@ import java.util.UUID; */ public class CreatedTokenWatcher extends Watcher { + // Player ID to Number of tokens created private final Map playerMap = new HashMap<>(); + private final Map, Integer>> tokenCreatedMap = new HashMap<>(); public CreatedTokenWatcher() { super(WatcherScope.GAME); @@ -25,12 +29,19 @@ public class CreatedTokenWatcher extends Watcher { public void watch(GameEvent event, Game game) { if (event.getType() == GameEvent.EventType.CREATED_TOKEN) { playerMap.compute(event.getPlayerId(), CardUtil::setOrIncrementValue); + + tokenCreatedMap.putIfAbsent(event.getPlayerId(), new HashMap<>()); + Class tokenClazz = ((Token) game.getPermanent(event.getTargetId())).getClass(); + Map, Integer> playersTokens = tokenCreatedMap.getOrDefault(event.getPlayerId(), new HashMap<>()); + playersTokens.compute(tokenClazz, CardUtil::setOrIncrementValue); + tokenCreatedMap.put(event.getPlayerId(), playersTokens); } } @Override public void reset() { playerMap.clear(); + tokenCreatedMap.clear(); } public static boolean checkPlayer(UUID playerId, Game game) { @@ -44,4 +55,13 @@ public class CreatedTokenWatcher extends Watcher { .playerMap .getOrDefault(playerId, 0); } + + public static int getTypeCreatedCountByPlayer(UUID playerId, Class tokenCLazz, Game game) { + return game + .getState() + .getWatcher(CreatedTokenWatcher.class) + .tokenCreatedMap + .getOrDefault(playerId, new HashMap<>()) + .getOrDefault(tokenCLazz, 0); + } } diff --git a/Mage/src/main/java/mage/watchers/common/DamageDoneWatcher.java b/Mage/src/main/java/mage/watchers/common/DamageDoneWatcher.java index 348f88f290..18642854c7 100644 --- a/Mage/src/main/java/mage/watchers/common/DamageDoneWatcher.java +++ b/Mage/src/main/java/mage/watchers/common/DamageDoneWatcher.java @@ -6,9 +6,7 @@ import mage.game.Game; import mage.game.events.GameEvent; import mage.watchers.Watcher; -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; +import java.util.*; /** * @author LevelX2 @@ -18,6 +16,12 @@ public class DamageDoneWatcher extends Watcher { // which object did how much damage during the turn private final Map damagingObjects; + // which object received how much damage during the turn + private final Map damagedObjects; + + // Which object damaged which player(s) + private final Map objectsToPlayersDamaged; + public Map getDamagingObjects() { return damagingObjects; } @@ -26,13 +30,13 @@ public class DamageDoneWatcher extends Watcher { return damagedObjects; } - // which object received how much damage during the turn - private final Map damagedObjects; + public DamageDoneWatcher() { super(WatcherScope.GAME); this.damagingObjects = new HashMap<>(); this.damagedObjects = new HashMap<>(); + this.objectsToPlayersDamaged = new HashMap<>(); } @Override @@ -47,6 +51,11 @@ public class DamageDoneWatcher extends Watcher { MageObjectReference damageTargetRef = new MageObjectReference(event.getTargetId(), game); damagedObjects.putIfAbsent(damageTargetRef, 0); damagedObjects.compute(damageTargetRef, (k, damage) -> damage + event.getAmount()); + + if (game.getPlayer(event.getTargetId()) != null) { + objectsToPlayersDamaged.putIfAbsent(damageSourceRef, 0); + objectsToPlayersDamaged.compute(damageSourceRef, (k, numPlayers) -> numPlayers + 1); + } } } } @@ -56,6 +65,7 @@ public class DamageDoneWatcher extends Watcher { super.reset(); damagingObjects.clear(); damagedObjects.clear(); + objectsToPlayersDamaged.clear(); } public int damageDoneBy(UUID objectId, int zoneChangeCounter, Game game) { @@ -73,4 +83,9 @@ public class DamageDoneWatcher extends Watcher { return damagedObjects.containsKey(mor); } + public boolean damagedAPlayer(UUID objectId, int zoneChangeCounter, Game game) { + MageObjectReference mor = new MageObjectReference(objectId, zoneChangeCounter, game); + return objectsToPlayersDamaged.containsKey(mor); + } + }