From cbb3012b056808fd8a1b94786d6acabad80204c6 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Fri, 21 Feb 2025 21:19:34 -0600 Subject: [PATCH 01/44] [DFT] Implement Ketramose, the New Dawn and implement exile condition and dynamic value --- .../src/mage/cards/k/KetramoseTheNewDawn.java | 113 ++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 4 + .../common/CardsInExileCondition.java | 34 ++++++ .../common/CardsInExileCount.java | 110 +++++++++++++++++ 4 files changed, 261 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java create mode 100644 Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java diff --git a/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java b/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java new file mode 100644 index 00000000000..f87abbbf1db --- /dev/null +++ b/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java @@ -0,0 +1,113 @@ +package mage.cards.k; + +import mage.MageInt; +import mage.abilities.BatchTriggeredAbility; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.CardsInExileCondition; +import mage.abilities.dynamicvalue.common.CardsInExileCount; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.LoseLifeSourceControllerEffect; +import mage.abilities.effects.common.combat.CantAttackBlockUnlessConditionSourceEffect; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +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.ZoneChangeBatchEvent; +import mage.game.events.ZoneChangeEvent; + +import java.util.UUID; + +/** + * @author Jmlundeen + */ +public final class KetramoseTheNewDawn extends CardImpl { + + public KetramoseTheNewDawn(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}{B}"); + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.GOD); + + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Menace + this.addAbility(new MenaceAbility()); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // Indestructible + this.addAbility(IndestructibleAbility.getInstance()); + + // Ketramose can't attack or block unless there are seven or more cards in exile. + this.addAbility(new SimpleStaticAbility( + new CantAttackBlockUnlessConditionSourceEffect(new CardsInExileCondition(ComparisonType.MORE_THAN, 6, CardsInExileCount.ALL)) + .setText("{this} can't attack or block unless there are seven or more cards in exile") + ).addHint(CardsInExileCount.ALL.getHint())); + + // Whenever one or more cards are put into exile from graveyards and/or the battlefield during your turn, you draw a card and lose 1 life. + this.addAbility(new KetramoseTriggeredAbility()); + + } + + private KetramoseTheNewDawn(final KetramoseTheNewDawn card) { + super(card); + } + + @Override + public KetramoseTheNewDawn copy() { + return new KetramoseTheNewDawn(this); + } + +} +class KetramoseTriggeredAbility extends TriggeredAbilityImpl implements BatchTriggeredAbility { + KetramoseTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); + this.addEffect(new LoseLifeSourceControllerEffect(1)); + } + + private KetramoseTriggeredAbility(final KetramoseTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; + } + + @Override + public boolean checkEvent(ZoneChangeEvent event, Game game) { + if (event.getToZone() != Zone.EXILED) { + return false; + } + return event.getFromZone() == Zone.GRAVEYARD || event.getFromZone() == Zone.BATTLEFIELD; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return game.getActivePlayerId().equals(getControllerId()) + && !getFilteredEvents((ZoneChangeBatchEvent) event, game).isEmpty(); + } + + @Override + public TriggeredAbility copy() + { + return new KetramoseTriggeredAbility(this); + } + + @Override + public String getRule() { + return "Whenever one or more cards are put into exile from graveyards" + + " and/or the battlefield during your turn, you draw a card and lose 1 life."; + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index dfcef6ecbec..5b3bee14ac7 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -131,6 +131,10 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Jungle Hollow", 256, Rarity.COMMON, mage.cards.j.JungleHollow.class)); cards.add(new SetCardInfo("Kalakscion, Hunger Tyrant", 93, Rarity.UNCOMMON, mage.cards.k.KalakscionHungerTyrant.class)); cards.add(new SetCardInfo("Keen Buccaneer", 48, Rarity.COMMON, mage.cards.k.KeenBuccaneer.class)); + cards.add(new SetCardInfo("Ketramose, the New Dawn", 209, Rarity.MYTHIC, mage.cards.k.KetramoseTheNewDawn.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ketramose, the New Dawn", 350, Rarity.MYTHIC, mage.cards.k.KetramoseTheNewDawn.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ketramose, the New Dawn", 482, Rarity.MYTHIC, mage.cards.k.KetramoseTheNewDawn.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ketramose, the New Dawn", 549, Rarity.MYTHIC, mage.cards.k.KetramoseTheNewDawn.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Kickoff Celebrations", 135, Rarity.COMMON, mage.cards.k.KickoffCelebrations.class)); cards.add(new SetCardInfo("Lagorin, Soul of Alacria", 211, Rarity.UNCOMMON, mage.cards.l.LagorinSoulOfAlacria.class)); cards.add(new SetCardInfo("Leonin Surveyor", 18, Rarity.COMMON, mage.cards.l.LeoninSurveyor.class)); diff --git a/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java b/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java new file mode 100644 index 00000000000..6fa1ba75005 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java @@ -0,0 +1,34 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.dynamicvalue.common.CardsInExileCount; +import mage.constants.ComparisonType; +import mage.game.Game; + + +public class CardsInExileCondition implements Condition +{ + private final ComparisonType type; + private final int count; + private CardsInExileCount cardsInExileCount; + + public CardsInExileCondition(ComparisonType type, int count) + { + this(type, count, CardsInExileCount.YOU); + } + + public CardsInExileCondition(ComparisonType type, int count, CardsInExileCount cardsInExileCount) + { + this.type = type; + this.count = count; + this.cardsInExileCount = cardsInExileCount; + } + + @Override + public boolean apply(Game game, Ability source) + { + int exileCards = cardsInExileCount.calculate(game, source, null); + return ComparisonType.compare(exileCards, type, count); + } +} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java new file mode 100644 index 00000000000..6db633c0c2f --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java @@ -0,0 +1,110 @@ +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.hint.Hint; +import mage.cards.Card; +import mage.constants.CardType; +import mage.game.Game; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author JayDi85 + */ +public enum CardsInExileCount implements DynamicValue { + YOU("you"), + ALL("all players"), + OPPONENTS("your opponents'"); + + private final String message; + private final CardsInExileHint hint; + + CardsInExileCount(String message) { + this.message = "The number of cards owned by " + message + " in exile"; + this.hint = new CardsInExileHint(this); + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return getExileCards(game, sourceAbility) + .mapToInt(x -> 1) + .sum(); + } + + @Override + public CardsInExileCount copy() { + return this; + } + + @Override + public String toString() { + return "X"; + } + + @Override + public String getMessage() { + return message; + } + + public Hint getHint() { + return hint; + } + + public Stream getExileCards(Game game, Ability ability) { + Collection playerIds; + switch (this) { + case YOU: + playerIds = Collections.singletonList(ability.getControllerId()); + break; + case OPPONENTS: + playerIds = game.getOpponents(ability.getControllerId()); + break; + case ALL: + playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game); + break; + default: + throw new IllegalArgumentException("Wrong code usage: miss implementation for " + this); + } + return playerIds.stream() + .map(game::getPlayer) + .filter(Objects::nonNull) + .map(player -> game.getExile().getAllCards(game, player.getId())) + .flatMap(Collection::stream) + .filter(Objects::nonNull); + } +} + +class CardsInExileHint implements Hint { + + CardsInExileCount value; + + CardsInExileHint(CardsInExileCount value) { + this.value = value; + } + + private CardsInExileHint(final CardsInExileHint hint) { + this.value = hint.value; + } + + @Override + public String getText(Game game, Ability ability) { + int count = value.getExileCards(game, ability) + .mapToInt(x -> 1) + .sum(); + + return this.value.getMessage() + ": " + count; + } + + @Override + public CardsInExileHint copy() { + return new CardsInExileHint(this); + } +} \ No newline at end of file From b9fa82f602ef138e16d5779cabf0da27496d1d14 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sat, 22 Feb 2025 14:13:08 -0600 Subject: [PATCH 02/44] Replace unnecessary custom hint class with ValueHint --- .../common/CardsInExileCount.java | 35 ++----------------- 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java index 6db633c0c2f..851747f81a8 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java @@ -4,16 +4,14 @@ import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; import mage.cards.Card; -import mage.constants.CardType; import mage.game.Game; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Objects; import java.util.UUID; -import java.util.stream.Collectors; import java.util.stream.Stream; /** @@ -25,11 +23,11 @@ public enum CardsInExileCount implements DynamicValue { OPPONENTS("your opponents'"); private final String message; - private final CardsInExileHint hint; + private final ValueHint hint; CardsInExileCount(String message) { this.message = "The number of cards owned by " + message + " in exile"; - this.hint = new CardsInExileHint(this); + this.hint = new ValueHint(this.message, this); } @Override @@ -80,31 +78,4 @@ public enum CardsInExileCount implements DynamicValue { .flatMap(Collection::stream) .filter(Objects::nonNull); } -} - -class CardsInExileHint implements Hint { - - CardsInExileCount value; - - CardsInExileHint(CardsInExileCount value) { - this.value = value; - } - - private CardsInExileHint(final CardsInExileHint hint) { - this.value = hint.value; - } - - @Override - public String getText(Game game, Ability ability) { - int count = value.getExileCards(game, ability) - .mapToInt(x -> 1) - .sum(); - - return this.value.getMessage() + ": " + count; - } - - @Override - public CardsInExileHint copy() { - return new CardsInExileHint(this); - } } \ No newline at end of file From 84ff720d2524232c6194bef6594bc35f3f339a22 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:52:53 -0500 Subject: [PATCH 03/44] fix text: Poultice Sliver --- Mage.Sets/src/mage/cards/p/PoulticeSliver.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Mage.Sets/src/mage/cards/p/PoulticeSliver.java b/Mage.Sets/src/mage/cards/p/PoulticeSliver.java index eca6543bcd4..402d738669e 100644 --- a/Mage.Sets/src/mage/cards/p/PoulticeSliver.java +++ b/Mage.Sets/src/mage/cards/p/PoulticeSliver.java @@ -1,7 +1,5 @@ - package mage.cards.p; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; @@ -13,18 +11,22 @@ import mage.abilities.effects.common.continuous.GainAbilityAllEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; import mage.constants.Duration; -import mage.constants.Zone; +import mage.constants.SubType; +import mage.filter.FilterPermanent; import mage.filter.StaticFilters; import mage.target.TargetPermanent; +import java.util.UUID; + /** * * @author anonymous */ public final class PoulticeSliver extends CardImpl { + private static final FilterPermanent filter = new FilterPermanent(SubType.SLIVER, "Sliver"); + public PoulticeSliver(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); this.subtype.add(SubType.SLIVER); @@ -35,7 +37,7 @@ public final class PoulticeSliver extends CardImpl { // All Slivers have "{2}, {tap}: Regenerate target Sliver." Ability ability = new SimpleActivatedAbility(new RegenerateTargetEffect(), new GenericManaCost(2)); ability.addCost(new TapSourceCost()); - ability.addTarget(new TargetPermanent(StaticFilters.FILTER_PERMANENT_ALL_SLIVERS)); + ability.addTarget(new TargetPermanent(filter)); this.addAbility(new SimpleStaticAbility( new GainAbilityAllEffect(ability, From 98897722bbc5813548e5546be88af6f38e1c7682 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Fri, 7 Mar 2025 00:41:51 -0600 Subject: [PATCH 04/44] [DFT] Implement Simple Cards (#13406) * [DFT] Implement Fang-Druid Summoner * [DFT] Implement March of the World Ooze * [DFT] Implement Riptide Gearhulk * [DFT] Implement Spectacular Pileup * [DFT] Implement Spire Mechcycle * fix Spire Mechcyle permanent filter * [DFT] Implement Thunderous Velocipede * [DFT] Implement Winter, Cursed Rider * [DFT] Implement Explosive Getaway * Missed PayLifeCost * Move "for each opponent" text --- .../src/mage/cards/e/ExplosiveGetaway.java | 51 ++++++++++++ .../src/mage/cards/f/FangDruidSummoner.java | 51 ++++++++++++ .../src/mage/cards/m/MarchOfTheWorldOoze.java | 58 +++++++++++++ .../src/mage/cards/r/RiptideGearhulk.java | 55 +++++++++++++ .../src/mage/cards/s/SpectacularPileup.java | 55 +++++++++++++ .../src/mage/cards/s/SpireMechcycle.java | 81 +++++++++++++++++++ .../mage/cards/t/ThunderousVelocipede.java | 71 ++++++++++++++++ .../src/mage/cards/w/WinterCursedRider.java | 76 +++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 29 +++++++ 9 files changed, 527 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/ExplosiveGetaway.java create mode 100644 Mage.Sets/src/mage/cards/f/FangDruidSummoner.java create mode 100644 Mage.Sets/src/mage/cards/m/MarchOfTheWorldOoze.java create mode 100644 Mage.Sets/src/mage/cards/r/RiptideGearhulk.java create mode 100644 Mage.Sets/src/mage/cards/s/SpectacularPileup.java create mode 100644 Mage.Sets/src/mage/cards/s/SpireMechcycle.java create mode 100644 Mage.Sets/src/mage/cards/t/ThunderousVelocipede.java create mode 100644 Mage.Sets/src/mage/cards/w/WinterCursedRider.java diff --git a/Mage.Sets/src/mage/cards/e/ExplosiveGetaway.java b/Mage.Sets/src/mage/cards/e/ExplosiveGetaway.java new file mode 100644 index 00000000000..5410a4bcbc1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/ExplosiveGetaway.java @@ -0,0 +1,51 @@ +package mage.cards.e; + +import java.util.UUID; +import java.util.function.Predicate; + +import mage.abilities.Ability; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.effects.common.DamageAllEffect; +import mage.abilities.effects.common.ExileReturnBattlefieldNextEndStepTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.filter.predicate.Predicates; +import mage.target.TargetPermanent; + +/** + * + * @author Jmlundeen + */ +public final class ExplosiveGetaway extends CardImpl { + private static final FilterPermanent filter = new FilterPermanent("artifact or creature"); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate() + )); + } + + public ExplosiveGetaway(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{R}{W}"); + + + // Exile up to one target artifact or creature. Return it to the battlefield under its owner's control at the beginning of the next end step. + this.getSpellAbility().addEffect(new ExileReturnBattlefieldNextEndStepTargetEffect().withTextThatCard(false)); + this.getSpellAbility().addTarget(new TargetPermanent(0, 1, filter)); + // Explosive Getaway deals 4 damage to each creature. + this.getSpellAbility().addEffect(new DamageAllEffect(4, StaticFilters.FILTER_PERMANENT_ALL_CREATURES).concatBy("
")); + } + + private ExplosiveGetaway(final ExplosiveGetaway card) { + super(card); + } + + @Override + public ExplosiveGetaway copy() { + return new ExplosiveGetaway(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FangDruidSummoner.java b/Mage.Sets/src/mage/cards/f/FangDruidSummoner.java new file mode 100644 index 00000000000..5aec37a1669 --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FangDruidSummoner.java @@ -0,0 +1,51 @@ +package mage.cards.f; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.search.SearchLibraryGraveyardPutInHandEffect; +import mage.constants.SubType; +import mage.abilities.keyword.ReachAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.NoAbilityPredicate; + +/** + * + * @author Jmlundeen + */ +public final class FangDruidSummoner extends CardImpl { + private static final FilterCreatureCard filter = new FilterCreatureCard("a creature card with no abilities"); + + static { + filter.add(NoAbilityPredicate.instance); + } + public FangDruidSummoner(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}"); + + this.subtype.add(SubType.APE); + this.subtype.add(SubType.DRUID); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Reach + this.addAbility(ReachAbility.getInstance()); + + // When this creature enters, you may search your library and/or graveyard for a creature card with no abilities, reveal it, and put it into your hand. If you search your library this way, shuffle. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new SearchLibraryGraveyardPutInHandEffect(filter, false, true) + )); + } + + private FangDruidSummoner(final FangDruidSummoner card) { + super(card); + } + + @Override + public FangDruidSummoner copy() { + return new FangDruidSummoner(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MarchOfTheWorldOoze.java b/Mage.Sets/src/mage/cards/m/MarchOfTheWorldOoze.java new file mode 100644 index 00000000000..7cb1ae1f446 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MarchOfTheWorldOoze.java @@ -0,0 +1,58 @@ +package mage.cards.m; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.SpellCastOpponentNoManaSpentTriggeredAbility; +import mage.abilities.common.SpellCastOpponentTriggeredAbility; +import mage.abilities.condition.common.OnOpponentsTurnCondition; +import mage.abilities.effects.common.CastSourceTriggeredAbility; +import mage.abilities.effects.common.CreateTokenAllEffect; +import mage.abilities.effects.common.CreateTokenControllerTargetEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.AddCardSubtypeAllEffect; +import mage.abilities.effects.common.continuous.SetBasePowerToughnessAllEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterSpell; +import mage.filter.StaticFilters; +import mage.game.permanent.token.ElephantToken; + +/** + * + * @author Jmlundeen + */ +public final class MarchOfTheWorldOoze extends CardImpl { + public static final FilterSpell filter = new FilterSpell("a spell, if it's not their turn"); + + static { + filter.add(TargetController.INACTIVE.getControllerPredicate()); + } + public MarchOfTheWorldOoze(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}{G}{G}"); + + + // Creatures you control have base power and toughness 6/6 and are Oozes in addition to their other types. + Ability ability = new SimpleStaticAbility(new SetBasePowerToughnessAllEffect( + 6, 6, Duration.WhileOnBattlefield, StaticFilters.FILTER_CONTROLLED_CREATURE)); + ability.addEffect(new AddCardSubtypeAllEffect(StaticFilters.FILTER_CONTROLLED_CREATURE, SubType.OOZE, null) + .concatBy("and")); + this.addAbility(ability); + // Whenever an opponent casts a spell, if it's not their turn, you create a 3/3 green Elephant creature token. + Ability ability2 = new SpellCastOpponentTriggeredAbility(new CreateTokenEffect(new ElephantToken()) + .setText("you create a 3/3 green Elephant creature token"), + filter, false); + this.addAbility(ability2); + } + + private MarchOfTheWorldOoze(final MarchOfTheWorldOoze card) { + super(card); + } + + @Override + public MarchOfTheWorldOoze copy() { + return new MarchOfTheWorldOoze(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RiptideGearhulk.java b/Mage.Sets/src/mage/cards/r/RiptideGearhulk.java new file mode 100644 index 00000000000..cdcf4c2a575 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiptideGearhulk.java @@ -0,0 +1,55 @@ +package mage.cards.r; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.PutIntoLibraryNFromTopTargetEffect; +import mage.constants.SubType; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.abilities.keyword.ProwessAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.common.TargetNonlandPermanent; +import mage.target.targetadjustment.ForEachOpponentTargetsAdjuster; +import mage.target.targetpointer.EachTargetPointer; + +/** + * + * @author Jmlundeen + */ +public final class RiptideGearhulk extends CardImpl { + public RiptideGearhulk(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{1}{W}{W}{U}{U}"); + + this.subtype.add(SubType.CONSTRUCT); + this.power = new MageInt(2); + this.toughness = new MageInt(5); + + // Double strike + this.addAbility(DoubleStrikeAbility.getInstance()); + + // Prowess + this.addAbility(new ProwessAbility()); + + // When this creature enters, for each opponent, put up to one target nonland permanent that player controls into its owner's library third from the top. + Effect effect = new PutIntoLibraryNFromTopTargetEffect(3) + .setText("for each opponent, put up to one target nonland permanent that player controls into its owner's library third from the top") + .setTargetPointer(new EachTargetPointer()); + Ability ability = new EntersBattlefieldTriggeredAbility(effect); + ability.addTarget(new TargetNonlandPermanent(0, 1)); + ability.setTargetAdjuster(new ForEachOpponentTargetsAdjuster()); + this.addAbility(ability); + } + + private RiptideGearhulk(final RiptideGearhulk card) { + super(card); + } + + @Override + public RiptideGearhulk copy() { + return new RiptideGearhulk(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SpectacularPileup.java b/Mage.Sets/src/mage/cards/s/SpectacularPileup.java new file mode 100644 index 00000000000..133b833404e --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SpectacularPileup.java @@ -0,0 +1,55 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DestroyAllEffect; +import mage.abilities.effects.common.continuous.LoseAbilityAllEffect; +import mage.abilities.keyword.CyclingAbility; +import mage.abilities.keyword.IndestructibleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; + +/** + * + * @author Jmlundeen + */ +public final class SpectacularPileup extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent("creatures and Vehicles"); + + static { + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + public SpectacularPileup(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{W}{W}"); + + + // All creatures and Vehicles lose indestructible until end of turn, then destroy all creatures and Vehicles. + this.getSpellAbility().addEffect(new LoseAbilityAllEffect( + IndestructibleAbility.getInstance(), Duration.EndOfTurn, filter) + .setText("All creatures and Vehicles lose indestructible until end of turn") + ); + this.getSpellAbility().addEffect(new DestroyAllEffect(filter).concatBy(", then")); + // Cycling {2} + this.addAbility(new CyclingAbility(new ManaCostsImpl<>("{2}"))); + + } + + private SpectacularPileup(final SpectacularPileup card) { + super(card); + } + + @Override + public SpectacularPileup copy() { + return new SpectacularPileup(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SpireMechcycle.java b/Mage.Sets/src/mage/cards/s/SpireMechcycle.java new file mode 100644 index 00000000000..f92e9a425c3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SpireMechcycle.java @@ -0,0 +1,81 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.costs.common.TapTargetCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.continuous.AddCardTypeSourceEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.CrewAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.filter.predicate.permanent.TappedPredicate; +import mage.target.common.TargetControlledPermanent; + +/** + * + * @author Jmlundeen + */ +public final class SpireMechcycle extends CardImpl { + + private static final FilterControlledPermanent filter = new FilterControlledPermanent("another untapped Mount or Vehicle you control"); + private static final FilterControlledPermanent mountAndVehicleFilter = new FilterControlledPermanent("Mount and/or Vehicle you control other than this Vehicle"); + private static final DynamicValue mountAndVehicleCount = new PermanentsOnBattlefieldCount(mountAndVehicleFilter); + + static { + filter.add(AnotherPredicate.instance); + filter.add(TappedPredicate.UNTAPPED); + filter.add(Predicates.or( + SubType.MOUNT.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + mountAndVehicleFilter.add(AnotherPredicate.instance); + mountAndVehicleFilter.add(Predicates.or( + SubType.MOUNT.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + public SpireMechcycle(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}{R}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // Exhaust -- Tap another untapped Mount or Vehicle you control: This Vehicle becomes an artifact creature. Put a +1/+1 counter on it for each Mount and/or Vehicle you control other than this Vehicle. + Effect effect = new AddCardTypeSourceEffect(Duration.WhileOnBattlefield, CardType.ARTIFACT, CardType.CREATURE) + .setText("This Vehicle becomes an artifact creature"); + Ability ability = new ExhaustAbility(effect, new TapTargetCost(new TargetControlledPermanent(filter))); + ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(), mountAndVehicleCount) + .setText("Put a +1/+1 counter on it for each Mount and/or Vehicle you control other than this Vehicle")); + this.addAbility(ability); + // Crew 2 + this.addAbility(new CrewAbility(2)); + + } + + private SpireMechcycle(final SpireMechcycle card) { + super(card); + } + + @Override + public SpireMechcycle copy() { + return new SpireMechcycle(this); + } +} diff --git a/Mage.Sets/src/mage/cards/t/ThunderousVelocipede.java b/Mage.Sets/src/mage/cards/t/ThunderousVelocipede.java new file mode 100644 index 00000000000..60bf8ff8b4d --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/ThunderousVelocipede.java @@ -0,0 +1,71 @@ +package mage.cards.t; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.EntersWithCountersControlledEffect; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.CrewAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.ManaValuePredicate; + +/** + * + * @author Jmlundeen + */ +public final class ThunderousVelocipede extends CardImpl { + + private static final FilterPermanent fourOrLessFilter = new FilterPermanent("other Vehicle and creature you control with mana value 4 or less"); + private static final FilterPermanent greaterThanFourFilter = new FilterPermanent("other Vehicle and creature you control with mana value greater than 4"); + + static { + fourOrLessFilter.add(new ManaValuePredicate(ComparisonType.OR_LESS, 4)); + fourOrLessFilter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + greaterThanFourFilter.add(new ManaValuePredicate(ComparisonType.MORE_THAN, 4)); + greaterThanFourFilter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + + public ThunderousVelocipede(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{G}{G}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Each other Vehicle and creature you control enters with an additional +1/+1 counter on it if its mana value is 4 or less. Otherwise, it enters with three additional +1/+1 counters on it. + Ability ability = new SimpleStaticAbility(new EntersWithCountersControlledEffect(fourOrLessFilter, CounterType.P1P1.createInstance(1), true) + .setText("Each other Vehicle and creature you control enters with an additional +1/+1 counter on it if its mana value is 4 or less.")); + ability.addEffect(new EntersWithCountersControlledEffect(greaterThanFourFilter, CounterType.P1P1.createInstance(3), true) + .setText("otherwise, it enters with three additional +1/+1 counters on it.")); + this.addAbility(ability); + // Crew 3 + this.addAbility(new CrewAbility(3)); + + } + + private ThunderousVelocipede(final ThunderousVelocipede card) { + super(card); + } + + @Override + public ThunderousVelocipede copy() { + return new ThunderousVelocipede(this); + } +} diff --git a/Mage.Sets/src/mage/cards/w/WinterCursedRider.java b/Mage.Sets/src/mage/cards/w/WinterCursedRider.java new file mode 100644 index 00000000000..c2fea8bd0f0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WinterCursedRider.java @@ -0,0 +1,76 @@ +package mage.cards.w; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.ExileXFromYourGraveCost; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; +import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.abilities.effects.common.continuous.GainAbilityAllEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; + +/** + * + * @author Jmlundeen + */ +public final class WinterCursedRider extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("Each other nonartifact creature"); + private static final DynamicValue xValue = new SignInversionDynamicValue(GetXValue.instance); + + static { + filter.add(Predicates.not(CardType.ARTIFACT.getPredicate())); + } + + public WinterCursedRider(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(3); + this.toughness = new MageInt(2); + + // Ward--Pay 2 life. + this.addAbility(new WardAbility(new PayLifeCost(2))); + + // Artifacts you control have "Ward--Pay 2 life." + WardAbility wardAbility = new WardAbility(new PayLifeCost(2)); + this.addAbility(new SimpleStaticAbility( + new GainAbilityAllEffect(wardAbility, Duration.WhileOnBattlefield, StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACTS) + .setText("Artifacts you control have " + "\"" + wardAbility.getRuleWithoutHint() + "\"") + )); + // Exhaust -- {2}{U}{B}, {T}, Exile X artifact cards from your graveyard: Each other nonartifact creature gets -X/-X until end of turn. + Ability ability = new ExhaustAbility(new BoostAllEffect(xValue, xValue, Duration.EndOfTurn, filter, true), + new ManaCostsImpl<>("{2}{U}{B}")); + ability.addCost(new TapSourceCost()); + ability.addCost(new ExileXFromYourGraveCost(StaticFilters.FILTER_CARD_ARTIFACTS) + .setText("Exile X artifact cards from your graveyard")); + this.addAbility(ability); + } + + private WinterCursedRider(final WinterCursedRider card) { + super(card); + } + + @Override + public WinterCursedRider copy() { + return new WinterCursedRider(this); + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index fa0196632dd..cf4f88fbc67 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -111,7 +111,13 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Endrider Catalyzer", 124, Rarity.COMMON, mage.cards.e.EndriderCatalyzer.class)); cards.add(new SetCardInfo("Endrider Spikespitter", 125, Rarity.UNCOMMON, mage.cards.e.EndriderSpikespitter.class)); cards.add(new SetCardInfo("Engine Rat", 84, Rarity.COMMON, mage.cards.e.EngineRat.class)); + cards.add(new SetCardInfo("Explosive Getaway", 202, Rarity.RARE, mage.cards.e.ExplosiveGetaway.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Explosive Getaway", 390, Rarity.RARE, mage.cards.e.ExplosiveGetaway.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Explosive Getaway", 403, Rarity.MYTHIC, mage.cards.e.ExplosiveGetaway.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Explosive Getaway", 413, Rarity.MYTHIC, mage.cards.e.ExplosiveGetaway.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Explosive Getaway", 479, Rarity.RARE, mage.cards.e.ExplosiveGetaway.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Fang Guardian", 162, Rarity.UNCOMMON, mage.cards.f.FangGuardian.class)); + cards.add(new SetCardInfo("Fang-Druid Summoner", 163, Rarity.UNCOMMON, mage.cards.f.FangDruidSummoner.class)); cards.add(new SetCardInfo("Far Fortune, End Boss", 203, Rarity.RARE, mage.cards.f.FarFortuneEndBoss.class)); cards.add(new SetCardInfo("Fearless Swashbuckler", 204, Rarity.RARE, mage.cards.f.FearlessSwashbuckler.class)); cards.add(new SetCardInfo("Flood the Engine", 42, Rarity.COMMON, mage.cards.f.FloodTheEngine.class)); @@ -164,6 +170,11 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Lumbering Worldwagon", 168, Rarity.RARE, mage.cards.l.LumberingWorldwagon.class)); cards.add(new SetCardInfo("Magmakin Artillerist", 137, Rarity.COMMON, mage.cards.m.MagmakinArtillerist.class)); cards.add(new SetCardInfo("Marauding Mako", 138, Rarity.UNCOMMON, mage.cards.m.MaraudingMako.class)); + cards.add(new SetCardInfo("March of the World Ooze", 169, Rarity.MYTHIC, mage.cards.m.MarchOfTheWorldOoze.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("March of the World Ooze", 388, Rarity.MYTHIC, mage.cards.m.MarchOfTheWorldOoze.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("March of the World Ooze", 402, Rarity.MYTHIC, mage.cards.m.MarchOfTheWorldOoze.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("March of the World Ooze", 412, Rarity.MYTHIC, mage.cards.m.MarchOfTheWorldOoze.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("March of the World Ooze", 468, Rarity.MYTHIC, mage.cards.m.MarchOfTheWorldOoze.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Marketback Walker", 235, Rarity.RARE, mage.cards.m.MarketbackWalker.class)); cards.add(new SetCardInfo("Marshals' Pathcruiser", 236, Rarity.UNCOMMON, mage.cards.m.MarshalsPathcruiser.class)); cards.add(new SetCardInfo("Maximum Overdrive", 96, Rarity.COMMON, mage.cards.m.MaximumOverdrive.class)); @@ -207,6 +218,10 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Repurposing Bay", 56, Rarity.RARE, mage.cards.r.RepurposingBay.class)); cards.add(new SetCardInfo("Ride's End", 25, Rarity.COMMON, mage.cards.r.RidesEnd.class)); cards.add(new SetCardInfo("Ripclaw Wrangler", 101, Rarity.COMMON, mage.cards.r.RipclawWrangler.class)); + cards.add(new SetCardInfo("Riptide Gearhulk", 219, Rarity.MYTHIC, mage.cards.r.RiptideGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Riptide Gearhulk", 353, Rarity.MYTHIC, mage.cards.r.RiptideGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Riptide Gearhulk", 490, Rarity.MYTHIC, mage.cards.r.RiptideGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Riptide Gearhulk", 552, Rarity.MYTHIC, mage.cards.r.RiptideGearhulk.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Rise from the Wreck", 178, Rarity.UNCOMMON, mage.cards.r.RiseFromTheWreck.class)); cards.add(new SetCardInfo("Risen Necroregent", 102, Rarity.UNCOMMON, mage.cards.r.RisenNecroregent.class)); cards.add(new SetCardInfo("Risky Shortcut", 103, Rarity.COMMON, mage.cards.r.RiskyShortcut.class)); @@ -232,10 +247,17 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Skycrash", 146, Rarity.UNCOMMON, mage.cards.s.Skycrash.class)); cards.add(new SetCardInfo("Skystreak Engineer", 61, Rarity.COMMON, mage.cards.s.SkystreakEngineer.class)); cards.add(new SetCardInfo("Slick Imitator", 62, Rarity.UNCOMMON, mage.cards.s.SlickImitator.class)); + cards.add(new SetCardInfo("Spectacular Pileup", 29, Rarity.RARE, mage.cards.s.SpectacularPileup.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Spectacular Pileup", 378, Rarity.RARE, mage.cards.s.SpectacularPileup.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Spectacular Pileup", 398, Rarity.MYTHIC, mage.cards.s.SpectacularPileup.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Spectacular Pileup", 408, Rarity.MYTHIC, mage.cards.s.SpectacularPileup.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Spectacular Pileup", 433, Rarity.RARE, mage.cards.s.SpectacularPileup.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Spectral Interference", 63, Rarity.COMMON, mage.cards.s.SpectralInterference.class)); cards.add(new SetCardInfo("Spell Pierce", 64, Rarity.UNCOMMON, mage.cards.s.SpellPierce.class)); cards.add(new SetCardInfo("Spikeshell Harrier", 65, Rarity.UNCOMMON, mage.cards.s.SpikeshellHarrier.class)); cards.add(new SetCardInfo("Spin Out", 106, Rarity.COMMON, mage.cards.s.SpinOut.class)); + cards.add(new SetCardInfo("Spire Mechcycle", 147, Rarity.UNCOMMON, mage.cards.s.SpireMechcycle.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Spire Mechcycle", 314, Rarity.UNCOMMON, mage.cards.s.SpireMechcycle.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Spotcycle Scouter", 30, Rarity.COMMON, mage.cards.s.SpotcycleScouter.class)); cards.add(new SetCardInfo("Stall Out", 66, Rarity.COMMON, mage.cards.s.StallOut.class)); cards.add(new SetCardInfo("Stampeding Scurryfoot", 181, Rarity.COMMON, mage.cards.s.StampedingScurryfoot.class)); @@ -256,6 +278,10 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Thornwood Falls", 266, Rarity.COMMON, mage.cards.t.ThornwoodFalls.class)); cards.add(new SetCardInfo("Thunderhead Gunner", 148, Rarity.COMMON, mage.cards.t.ThunderheadGunner.class)); cards.add(new SetCardInfo("Thundering Broodwagon", 225, Rarity.UNCOMMON, mage.cards.t.ThunderingBroodwagon.class)); + cards.add(new SetCardInfo("Thunderous Velocipede", 183, Rarity.MYTHIC, mage.cards.t.ThunderousVelocipede.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Thunderous Velocipede", 317, Rarity.MYTHIC, mage.cards.t.ThunderousVelocipede.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Thunderous Velocipede", 471, Rarity.MYTHIC, mage.cards.t.ThunderousVelocipede.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Thunderous Velocipede", 529, Rarity.MYTHIC, mage.cards.t.ThunderousVelocipede.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Ticket Tortoise", 245, Rarity.COMMON, mage.cards.t.TicketTortoise.class)); cards.add(new SetCardInfo("Trade the Helm", 69, Rarity.UNCOMMON, mage.cards.t.TradeTheHelm.class)); cards.add(new SetCardInfo("Tranquil Cove", 267, Rarity.COMMON, mage.cards.t.TranquilCove.class)); @@ -279,6 +305,9 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Wild Roads", 269, Rarity.UNCOMMON, mage.cards.w.WildRoads.class)); cards.add(new SetCardInfo("Willowrush Verge", 270, Rarity.RARE, mage.cards.w.WillowrushVerge.class)); cards.add(new SetCardInfo("Wind-Scarred Crag", 271, Rarity.COMMON, mage.cards.w.WindScarredCrag.class)); + cards.add(new SetCardInfo("Winter, Cursed Rider", 228, Rarity.RARE, mage.cards.w.WinterCursedRider.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Winter, Cursed Rider", 369, Rarity.RARE, mage.cards.w.WinterCursedRider.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Winter, Cursed Rider", 494, Rarity.RARE, mage.cards.w.WinterCursedRider.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Wreck Remover", 247, Rarity.COMMON, mage.cards.w.WreckRemover.class)); cards.add(new SetCardInfo("Wreckage Wickerfolk", 110, Rarity.COMMON, mage.cards.w.WreckageWickerfolk.class)); cards.add(new SetCardInfo("Wretched Doll", 111, Rarity.UNCOMMON, mage.cards.w.WretchedDoll.class)); From a7428a6c9351a1980ea797e54d2770617183046b Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Fri, 7 Mar 2025 00:44:01 -0600 Subject: [PATCH 05/44] [DFT] Add Mendicant Core, Guidelight (#13412) --- .../mage/cards/m/MendicantCoreGuidelight.java | 69 +++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 3 + 2 files changed, 72 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MendicantCoreGuidelight.java diff --git a/Mage.Sets/src/mage/cards/m/MendicantCoreGuidelight.java b/Mage.Sets/src/mage/cards/m/MendicantCoreGuidelight.java new file mode 100644 index 00000000000..8825fd6713b --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MendicantCoreGuidelight.java @@ -0,0 +1,69 @@ +package mage.cards.m; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.MaxSpeedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.common.delayed.CopyNextSpellDelayedTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.CopyStackObjectEffect; +import mage.abilities.effects.common.CopyTargetStackObjectEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.continuous.SetBasePowerSourceEffect; +import mage.constants.*; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.FilterPermanent; +import mage.filter.FilterSpell; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; + +/** + * + * @author Jmlundeen + */ +public final class MendicantCoreGuidelight extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("an artifact spell"); + private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACTS); + + static { + filter.add(CardType.ARTIFACT.getPredicate()); + } + + public MendicantCoreGuidelight(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{W}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ROBOT); + this.power = new MageInt(0); + this.toughness = new MageInt(3); + + // Mendicant Core's power is equal to the number of artifacts you control. + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new SetBasePowerSourceEffect(xValue))); + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // Max speed -- Whenever you cast an artifact spell, you may pay {1}. If you do, copy it. + Effect copyEffect = new CopyTargetStackObjectEffect(true) + .setText("copy it. (The copy becomes a token.)"); + Effect doIfEffect = new DoIfCostPaid(copyEffect,new ManaCostsImpl<>("{1}")); + Ability ability = new SpellCastControllerTriggeredAbility(doIfEffect, filter, false, SetTargetPointer.SPELL); + this.addAbility(new MaxSpeedAbility(ability)); + } + + private MendicantCoreGuidelight(final MendicantCoreGuidelight card) { + super(card); + } + + @Override + public MendicantCoreGuidelight copy() { + return new MendicantCoreGuidelight(this); + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index cf4f88fbc67..e311a06905e 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -179,6 +179,9 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Marshals' Pathcruiser", 236, Rarity.UNCOMMON, mage.cards.m.MarshalsPathcruiser.class)); cards.add(new SetCardInfo("Maximum Overdrive", 96, Rarity.COMMON, mage.cards.m.MaximumOverdrive.class)); cards.add(new SetCardInfo("Memory Guardian", 49, Rarity.UNCOMMON, mage.cards.m.MemoryGuardian.class)); + cards.add(new SetCardInfo("Mendicant Core, Guidelight", 213, Rarity.RARE, mage.cards.m.MendicantCoreGuidelight.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mendicant Core, Guidelight", 365, Rarity.RARE, mage.cards.m.MendicantCoreGuidelight.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mendicant Core, Guidelight", 485, Rarity.RARE, mage.cards.m.MendicantCoreGuidelight.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Midnight Mangler", 50, Rarity.COMMON, mage.cards.m.MidnightMangler.class)); cards.add(new SetCardInfo("Migrating Ketradon", 170, Rarity.COMMON, mage.cards.m.MigratingKetradon.class)); cards.add(new SetCardInfo("Mindspring Merfolk", 51, Rarity.RARE, mage.cards.m.MindspringMerfolk.class)); From 18debb7ae3f68307e6b7ba6c9b1595d3e6b706a3 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:58:36 -0500 Subject: [PATCH 06/44] fix #13414 (Fecund Greenshell) --- Mage.Sets/src/mage/cards/f/FecundGreenshell.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/f/FecundGreenshell.java b/Mage.Sets/src/mage/cards/f/FecundGreenshell.java index 995d4fd4828..3a88f596fd6 100644 --- a/Mage.Sets/src/mage/cards/f/FecundGreenshell.java +++ b/Mage.Sets/src/mage/cards/f/FecundGreenshell.java @@ -59,7 +59,7 @@ public final class FecundGreenshell extends CardImpl { // Whenever Fecund Greenshell or another creature you control with toughness greater than its power enters, // look at the top card of your library. If it's a land card, you may put it onto the battlefield tapped. Otherwise, put it into your hand. this.addAbility(new EntersBattlefieldThisOrAnotherTriggeredAbility( - new FecundGreenshellEffect(), filter, false, false)); + new FecundGreenshellEffect(), filter, false, true)); } private FecundGreenshell(final FecundGreenshell card) { From f9aa8c1527224622dc6c02ebb42fe8692656d073 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Sat, 8 Mar 2025 09:49:29 -0600 Subject: [PATCH 07/44] [DFT] Implement Wickerfolk Indomitable (#13404) --- .../mage/cards/w/WickerfolkIndomitable.java | 94 +++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 1 + Mage/src/main/java/mage/MageIdentifier.java | 3 +- 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/w/WickerfolkIndomitable.java diff --git a/Mage.Sets/src/mage/cards/w/WickerfolkIndomitable.java b/Mage.Sets/src/mage/cards/w/WickerfolkIndomitable.java new file mode 100644 index 00000000000..02199864ec4 --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WickerfolkIndomitable.java @@ -0,0 +1,94 @@ +package mage.cards.w; + +import java.util.UUID; + +import mage.MageIdentifier; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.MayCastFromGraveyardSourceAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.Cost; +import mage.abilities.costs.Costs; +import mage.abilities.costs.CostsImpl; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; + +/** + * + * @author Jmlundeen + */ +public final class WickerfolkIndomitable extends CardImpl { + + public WickerfolkIndomitable(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}{B}"); + + this.subtype.add(SubType.SCARECROW); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // You may cast this card from your graveyard by paying 2 life and sacrificing an artifact or creature in addition to paying its other costs. + Ability ability = new SimpleStaticAbility(Zone.GRAVEYARD, new WickerfolkIndomitableGraveyardEffect()) + .setIdentifier(MageIdentifier.WickerfolkIndomitableAlternateCast); + + this.addAbility(ability); + } + + private WickerfolkIndomitable(final WickerfolkIndomitable card) { + super(card); + } + + @Override + public WickerfolkIndomitable copy() { + return new WickerfolkIndomitable(this); + } +} + +class WickerfolkIndomitableGraveyardEffect extends AsThoughEffectImpl { + + WickerfolkIndomitableGraveyardEffect() { + super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.PutCreatureInPlay); + this.staticText = "You may cast this card from your graveyard by paying 2 life and sacrificing an artifact or creature in addition to paying its other costs."; + } + + private WickerfolkIndomitableGraveyardEffect(final WickerfolkIndomitableGraveyardEffect effect) { + super(effect); + } + + @Override + public WickerfolkIndomitableGraveyardEffect copy() { + return new WickerfolkIndomitableGraveyardEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + if (!objectId.equals(source.getSourceId()) || !source.isControlledBy(affectedControllerId) + || game.getState().getZone(source.getSourceId()) != Zone.GRAVEYARD) { + return false; + } + Player controller = game.getPlayer(affectedControllerId); + if (controller != null) { + Costs costs = new CostsImpl<>(); + costs.add(new PayLifeCost(2)); + costs.add(new SacrificeTargetCost(StaticFilters.FILTER_CONTROLLED_PERMANENT_ARTIFACT_OR_CREATURE)); + controller.setCastSourceIdWithAlternateMana(objectId, new ManaCostsImpl<>("{3}{B}"), costs, + MageIdentifier.WickerfolkIndomitableAlternateCast); + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index e311a06905e..f16262aba1f 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -305,6 +305,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Wastewood Verge", 268, Rarity.RARE, mage.cards.w.WastewoodVerge.class)); cards.add(new SetCardInfo("Waxen Shapethief", 74, Rarity.RARE, mage.cards.w.WaxenShapethief.class)); cards.add(new SetCardInfo("Webstrike Elite", 186, Rarity.RARE, mage.cards.w.WebstrikeElite.class)); + cards.add(new SetCardInfo("Wickerfolk Indomitable", 109, Rarity.UNCOMMON, mage.cards.w.WickerfolkIndomitable.class)); cards.add(new SetCardInfo("Wild Roads", 269, Rarity.UNCOMMON, mage.cards.w.WildRoads.class)); cards.add(new SetCardInfo("Willowrush Verge", 270, Rarity.RARE, mage.cards.w.WillowrushVerge.class)); cards.add(new SetCardInfo("Wind-Scarred Crag", 271, Rarity.COMMON, mage.cards.w.WindScarredCrag.class)); diff --git a/Mage/src/main/java/mage/MageIdentifier.java b/Mage/src/main/java/mage/MageIdentifier.java index afb1416331e..26059236f48 100644 --- a/Mage/src/main/java/mage/MageIdentifier.java +++ b/Mage/src/main/java/mage/MageIdentifier.java @@ -76,7 +76,8 @@ public enum MageIdentifier { TheRuinousPowersAlternateCast, FiresOfMountDoomAlternateCast, PrimalPrayersAlternateCast, - QuilledGreatwurmAlternateCast; + QuilledGreatwurmAlternateCast, + WickerfolkIndomitableAlternateCast; /** * Additional text if there is need to differentiate two very similar effects From f22755d44d623540b7724ac13172145676bf83df Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Sat, 8 Mar 2025 11:38:26 -0600 Subject: [PATCH 08/44] [DFT] Implement Riverchurn Monument (#13405) * fix MillCardsTargetEffect to work with multiple targets --- .../src/mage/cards/r/RiverchurnMonument.java | 88 +++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 3 + .../effects/common/MillCardsTargetEffect.java | 13 +-- 3 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/r/RiverchurnMonument.java diff --git a/Mage.Sets/src/mage/cards/r/RiverchurnMonument.java b/Mage.Sets/src/mage/cards/r/RiverchurnMonument.java new file mode 100644 index 00000000000..d9f211b9bda --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RiverchurnMonument.java @@ -0,0 +1,88 @@ +package mage.cards.r; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.MillCardsTargetEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPlayer; +import mage.target.targetpointer.EachTargetPointer; + +/** + * + * @author Jmlundeen + */ +public final class RiverchurnMonument extends CardImpl { + + public RiverchurnMonument(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{U}"); + + + // {1}, {T}: Any number of target players each mill two cards. + Effect effect = new MillCardsTargetEffect(2); + effect.setTargetPointer(new EachTargetPointer()); + effect.setText("Any number of target players each mill two cards. " + + "(Each of them puts the top two cards of their library into their graveyard.)"); + Ability ability = new SimpleActivatedAbility(effect, new ManaCostsImpl<>("{1}")); + ability.addCost(new TapSourceCost()); + ability.addTarget(new TargetPlayer(0, Integer.MAX_VALUE, false)); + this.addAbility(ability); + + // Exhaust -- {2}{U}{U}, {T}: Any number of target players each mill cards equal to the number of cards in their graveyard. + Effect exhaustEffect = new RiverchurnMonumentEffect(); + exhaustEffect.setTargetPointer(new EachTargetPointer()); + exhaustEffect.setText("Any number of target players each mill cards equal to the number of cards in their graveyard."); + Ability exhaustAbility = new ExhaustAbility(exhaustEffect, new ManaCostsImpl<>("{2}{U}{U}")); + exhaustAbility.addCost(new TapSourceCost()); + exhaustAbility.addTarget(new TargetPlayer(0, Integer.MAX_VALUE, false)); + this.addAbility(exhaustAbility); + } + + private RiverchurnMonument(final RiverchurnMonument card) { + super(card); + } + + @Override + public RiverchurnMonument copy() { + return new RiverchurnMonument(this); + } +} + +class RiverchurnMonumentEffect extends OneShotEffect { + + public RiverchurnMonumentEffect() { + super(Outcome.Detriment); + this.staticText = "Any number of target players each mill cards equal to the number of cards in their graveyard."; + } + + public RiverchurnMonumentEffect(final RiverchurnMonumentEffect effect) { + super(effect); + } + + @Override + public RiverchurnMonumentEffect copy() { + return new RiverchurnMonumentEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID playerId : getTargetPointer().getTargets(game, source)) { + Player player = game.getPlayer(playerId); + if (player != null) { + player.millCards(player.getGraveyard().size(), source, game); + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index f16262aba1f..76d348b8c14 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -228,6 +228,9 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Rise from the Wreck", 178, Rarity.UNCOMMON, mage.cards.r.RiseFromTheWreck.class)); cards.add(new SetCardInfo("Risen Necroregent", 102, Rarity.UNCOMMON, mage.cards.r.RisenNecroregent.class)); cards.add(new SetCardInfo("Risky Shortcut", 103, Rarity.COMMON, mage.cards.r.RiskyShortcut.class)); + cards.add(new SetCardInfo("Riverchurn Monument", 57, Rarity.RARE, mage.cards.r.RiverchurnMonument.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Riverchurn Monument", 381, Rarity.RARE, mage.cards.r.RiverchurnMonument.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Riverchurn Monument", 440, Rarity.RARE, mage.cards.r.RiverchurnMonument.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Riverpyre Verge", 260, Rarity.RARE, mage.cards.r.RiverpyreVerge.class)); cards.add(new SetCardInfo("Road Rage", 145, Rarity.UNCOMMON, mage.cards.r.RoadRage.class)); cards.add(new SetCardInfo("Roadside Assistance", 26, Rarity.UNCOMMON, mage.cards.r.RoadsideAssistance.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/MillCardsTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/MillCardsTargetEffect.java index bd5fa91f6fa..25f32e64e2c 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/MillCardsTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/MillCardsTargetEffect.java @@ -10,6 +10,8 @@ import mage.game.Game; import mage.players.Player; import mage.util.CardUtil; +import java.util.UUID; + /** * @author LevelX2 */ @@ -38,12 +40,13 @@ public class MillCardsTargetEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(getTargetPointer().getFirst(game, source)); - if (player != null) { - player.millCards(numberCards.calculate(game, source, this), source, game); - return true; + for (UUID playerId : getTargetPointer().getTargets(game, source)) { + Player player = game.getPlayer(playerId); + if (player != null) { + player.millCards(numberCards.calculate(game, source, this), source, game); + } } - return false; + return true; } @Override From 121ab378e719ed2c045afe99f9f0370278da516e Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sat, 8 Mar 2025 12:52:39 -0600 Subject: [PATCH 09/44] Change CardsInExileCondition constructor and toString change CardsInExileCondition to accept generic DynamicValue create a toString override to generate rule text --- .../src/mage/cards/k/KetramoseTheNewDawn.java | 3 +- .../common/CardsInExileCondition.java | 39 +++++++++++++++++-- .../common/CardsInExileCount.java | 2 +- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java b/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java index f87abbbf1db..a319d936d67 100644 --- a/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java +++ b/Mage.Sets/src/mage/cards/k/KetramoseTheNewDawn.java @@ -51,8 +51,7 @@ public final class KetramoseTheNewDawn extends CardImpl { // Ketramose can't attack or block unless there are seven or more cards in exile. this.addAbility(new SimpleStaticAbility( - new CantAttackBlockUnlessConditionSourceEffect(new CardsInExileCondition(ComparisonType.MORE_THAN, 6, CardsInExileCount.ALL)) - .setText("{this} can't attack or block unless there are seven or more cards in exile") + new CantAttackBlockUnlessConditionSourceEffect(new CardsInExileCondition(ComparisonType.OR_GREATER, 7)) ).addHint(CardsInExileCount.ALL.getHint())); // Whenever one or more cards are put into exile from graveyards and/or the battlefield during your turn, you draw a card and lose 1 life. diff --git a/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java b/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java index 6fa1ba75005..1f8f4a3b784 100644 --- a/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java +++ b/Mage/src/main/java/mage/abilities/condition/common/CardsInExileCondition.java @@ -2,23 +2,30 @@ package mage.abilities.condition.common; import mage.abilities.Ability; import mage.abilities.condition.Condition; +import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CardsInExileCount; +import mage.abilities.dynamicvalue.common.StaticValue; import mage.constants.ComparisonType; import mage.game.Game; +import mage.util.CardUtil; - +/** + * Cards in exile condition + * + * @author Jmlundeen + */ public class CardsInExileCondition implements Condition { private final ComparisonType type; private final int count; - private CardsInExileCount cardsInExileCount; + private final DynamicValue cardsInExileCount; public CardsInExileCondition(ComparisonType type, int count) { - this(type, count, CardsInExileCount.YOU); + this(type, count, CardsInExileCount.ALL); } - public CardsInExileCondition(ComparisonType type, int count, CardsInExileCount cardsInExileCount) + public CardsInExileCondition(ComparisonType type, int count, DynamicValue cardsInExileCount) { this.type = type; this.count = count; @@ -31,4 +38,28 @@ public class CardsInExileCondition implements Condition int exileCards = cardsInExileCount.calculate(game, source, null); return ComparisonType.compare(exileCards, type, count); } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("there are "); + String countString = CardUtil.numberToText(count); + switch (type) { + case MORE_THAN: + sb.append("more than ").append(countString).append(" "); + break; + case FEWER_THAN: + sb.append("fewer than ").append(countString).append(" "); + break; + case OR_LESS: + sb.append(countString).append(" or less "); + break; + case OR_GREATER: + sb.append(countString).append(" or more "); + break; + default: + throw new IllegalArgumentException("comparison rules for " + type + " missing"); + } + sb.append("cards in exile"); + return sb.toString(); + } } diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java index 851747f81a8..2a6b6a1bfd7 100644 --- a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CardsInExileCount.java @@ -15,7 +15,7 @@ import java.util.UUID; import java.util.stream.Stream; /** - * @author JayDi85 + * @author Jmlundeen */ public enum CardsInExileCount implements DynamicValue { YOU("you"), From 65129a9d0a31a0d2513d7e1179b6a5f08e62eb5b Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Sat, 8 Mar 2025 21:49:10 -0500 Subject: [PATCH 10/44] [DSC] Fix Kianne, Corrupted Memory --- Mage.Sets/src/mage/cards/k/KianneCorruptedMemory.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mage.Sets/src/mage/cards/k/KianneCorruptedMemory.java b/Mage.Sets/src/mage/cards/k/KianneCorruptedMemory.java index a51151624c2..4b62095cda2 100644 --- a/Mage.Sets/src/mage/cards/k/KianneCorruptedMemory.java +++ b/Mage.Sets/src/mage/cards/k/KianneCorruptedMemory.java @@ -46,15 +46,15 @@ public final class KianneCorruptedMemory extends CardImpl { this.addAbility(new SimpleStaticAbility( new ConditionalAsThoughEffect( new CastAsThoughItHadFlashAllEffect(Duration.Custom, filter), - KianneCorruptedMemoryOddCondition.instance - ))); + KianneCorruptedMemoryEvenCondition.instance + ).setText("As long as {this}'s power is even, you may cast noncreature spells as though they had flash."))); // As long as Kianne's power is odd, you may cast creature spells as though they had flash. this.addAbility(new SimpleStaticAbility( new ConditionalAsThoughEffect( new CastAsThoughItHadFlashAllEffect(Duration.Custom, filter2), - KianneCorruptedMemoryEvenCondition.instance - ))); + KianneCorruptedMemoryOddCondition.instance + ).setText("As long as {this}'s power is odd, you may cast creature spells as though they had flash."))); // Whenever you draw a card, put a +1/+1 counter on Kianne. this.addAbility(new DrawCardControllerTriggeredAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance()), false)); From 5060f2504a0e6b10b5447eed12fe6c535ea46ebd Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Sat, 8 Mar 2025 23:21:40 -0500 Subject: [PATCH 11/44] [TDC] Implement Betor, Ancetor's Voice --- .../src/mage/cards/b/BetorAncestorsVoice.java | 99 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 1 + Utils/mtg-cards-data.txt | 1 + 3 files changed, 101 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BetorAncestorsVoice.java diff --git a/Mage.Sets/src/mage/cards/b/BetorAncestorsVoice.java b/Mage.Sets/src/mage/cards/b/BetorAncestorsVoice.java new file mode 100644 index 00000000000..88e1cac8d2a --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BetorAncestorsVoice.java @@ -0,0 +1,99 @@ +package mage.cards.b; + +import java.util.UUID; +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.ControllerGainedLifeCount; +import mage.abilities.dynamicvalue.common.ControllerLostLifeCount; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.target.common.TargetCardInGraveyard; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.targetpointer.SecondTargetPointer; +import mage.watchers.common.PlayerGainedLifeWatcher; + +/** + * + * @author Grath + */ +public final class BetorAncestorsVoice extends CardImpl { + + final private static FilterCard filter + = new FilterCard("creature card with mana value less than or equal to the amount of life you lost this turn"); + + static { + filter.add(CardType.CREATURE.getPredicate()); + filter.add(BetorPredicate.instance); + } + + public BetorAncestorsVoice(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{B}{G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(3); + this.toughness = new MageInt(5); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // At the beginning of your end step, put a number of +1/+1 counters on up to one other target creature you + // control equal to the amount of life you gained this turn. Return up to one target creature card with mana + // value less than or equal to the amount of life you lost this turn from your graveyard to the battlefield. + Ability ability = new BeginningOfEndStepTriggeredAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance(), ControllerGainedLifeCount.instance) + .setText("put a number of +1/+1 counters on up to one other target creature you " + + "control equal to the amount of life you gained this turn.") + ); + ability.addTarget(new TargetControlledCreaturePermanent(0, 1, StaticFilters.FILTER_ANOTHER_TARGET_CREATURE_YOU_CONTROL, false)); + ability.addEffect(new ReturnFromGraveyardToBattlefieldTargetEffect().setTargetPointer(new SecondTargetPointer()) + .setText("Return up to one target creature card with mana value less than or equal to the amount of " + + "life you lost this turn from your graveyard to the battlefield.")); + ability.addTarget(new TargetCardInGraveyard(0, 1, filter)); + ability.addHint(ControllerGainedLifeCount.getHint()); + ability.addHint(ControllerLostLifeCount.getHint()); + this.addAbility(ability, new PlayerGainedLifeWatcher()); + } + + private BetorAncestorsVoice(final BetorAncestorsVoice card) { + super(card); + } + + @Override + public BetorAncestorsVoice copy() { + return new BetorAncestorsVoice(this); + } +} + +enum BetorPredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return input.getObject().getManaValue() <= ControllerLostLifeCount.instance.calculate(game, input.getSource(), null); + } + + @Override + public String toString() { + return "mana value less than or equal to the amount of life you lost this turn"; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index b968e03fbf5..9e5f74393d1 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -19,6 +19,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { super("Tarkir: Dragonstorm Commander", "TDC", ExpansionSet.buildDate(2025, 4, 11), SetType.SUPPLEMENTAL); this.hasBasicLands = false; + cards.add(new SetCardInfo("Betor, Ancestor's Voice", 1, Rarity.MYTHIC, mage.cards.b.BetorAncestorsVoice.class)); cards.add(new SetCardInfo("Teval, the Balanced Scale", 8, Rarity.MYTHIC, mage.cards.t.TevalTheBalancedScale.class)); } } diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 66151efc0c2..d585e63d623 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -57207,3 +57207,4 @@ Shiko, Paragon of the Way|Tarkir: Dragonstorm|223|M|{2}{U}{R}{W}|Legendary Creat Skirmish Rhino|Tarkir: Dragonstorm|224|U|{W}{B}{G}|Creature - Rhino|3|4|Trample$When this creature enters, each opponent loses 2 life and you gain 2 life.| Mox Jasper|Tarkir: Dragonstorm|246|M|{0}|Legendary Artifact|||{T}: Add one mana of any color. Activate only if you control a Dragon.| Teval, the Balanced Scale|Tarkir: Dragonstorm Commander|8|M|{1}{B}{G}{U}|Legendary Creature - Spirit Dragon|4|4|Flying$Whenever Teval attacks, mill three cards. Then you may return a land card from your graveyard to the battlefield tapped.$Whenever one or more cards leave your graveyard, create a 2/2 black Zombie Druid creature token.| +Betor, Ancestor's Voice|Tarkir: Dragonstorm Commander|1|M|{2}{W}{B}{G}|Legendary Creature - Spirit Dragon|3|5|Flying, lifelink$At the beginning of your end step, put a number of +1/+1 counters on up to one other target creature you control equal to the amount of life you gained this turn. Return up to one target creature card with mana value less than or equal to the amount of life you lost this turn from your graveyard to the battlefield. From b614dcbcd6878f2754682010e404b5f0a5dc6dc4 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Mon, 10 Mar 2025 10:46:35 -0400 Subject: [PATCH 12/44] [BLC] Implement Evercoat Ursine. --- .../src/mage/cards/e/EvercoatUrsine.java | 50 +++++++++++++++++++ .../src/mage/sets/BloomburrowCommander.java | 1 + 2 files changed, 51 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/EvercoatUrsine.java diff --git a/Mage.Sets/src/mage/cards/e/EvercoatUrsine.java b/Mage.Sets/src/mage/cards/e/EvercoatUrsine.java new file mode 100644 index 00000000000..683ccc27666 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EvercoatUrsine.java @@ -0,0 +1,50 @@ +package mage.cards.e; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.effects.common.HideawayPlayEffect; +import mage.constants.SubType; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.keyword.HideawayAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; + +/** + * + * @author Grath + */ +public final class EvercoatUrsine extends CardImpl { + + public EvercoatUrsine(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}"); + + this.subtype.add(SubType.ELEMENTAL); + this.subtype.add(SubType.BEAR); + this.power = new MageInt(6); + this.toughness = new MageInt(5); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // Hideaway 3 + this.addAbility(new HideawayAbility(this, 3)); + + // Hideaway 3 + this.addAbility(new HideawayAbility(this, 3)); + + // Whenever Evercoat Ursine deals combat damage to a player, if there are cards exiled with it, you may play + // one of them without paying its mana cost. + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(new HideawayPlayEffect(true))); + } + + private EvercoatUrsine(final EvercoatUrsine card) { + super(card); + } + + @Override + public EvercoatUrsine copy() { + return new EvercoatUrsine(this); + } +} diff --git a/Mage.Sets/src/mage/sets/BloomburrowCommander.java b/Mage.Sets/src/mage/sets/BloomburrowCommander.java index cd669f6f14a..19461bbc1e5 100644 --- a/Mage.Sets/src/mage/sets/BloomburrowCommander.java +++ b/Mage.Sets/src/mage/sets/BloomburrowCommander.java @@ -97,6 +97,7 @@ public final class BloomburrowCommander extends ExpansionSet { cards.add(new SetCardInfo("End-Raze Forerunners", 214, Rarity.RARE, mage.cards.e.EndRazeForerunners.class)); cards.add(new SetCardInfo("Esika's Chariot", 215, Rarity.RARE, mage.cards.e.EsikasChariot.class)); cards.add(new SetCardInfo("Etali, Primal Storm", 196, Rarity.RARE, mage.cards.e.EtaliPrimalStorm.class)); + cards.add(new SetCardInfo("Evercoat Ursine", 30, Rarity.RARE, mage.cards.e.EvercoatUrsine.class)); cards.add(new SetCardInfo("Evolving Wilds", 302, Rarity.COMMON, mage.cards.e.EvolvingWilds.class)); cards.add(new SetCardInfo("Exotic Orchard", 131, Rarity.RARE, mage.cards.e.ExoticOrchard.class)); cards.add(new SetCardInfo("Explore", 216, Rarity.COMMON, mage.cards.e.Explore.class)); From 4e5bfee2799ffa3c07cc8d855d7c1bd494539e39 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:09:28 -0400 Subject: [PATCH 13/44] fix verify --- Mage.Sets/src/mage/sets/SecretLairDrop.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/sets/SecretLairDrop.java b/Mage.Sets/src/mage/sets/SecretLairDrop.java index 8636e57e720..63423a26ba4 100644 --- a/Mage.Sets/src/mage/sets/SecretLairDrop.java +++ b/Mage.Sets/src/mage/sets/SecretLairDrop.java @@ -361,7 +361,7 @@ public class SecretLairDrop extends ExpansionSet { cards.add(new SetCardInfo("Mountain", 362, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Forest", 363, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Swords to Plowshares", 364, Rarity.RARE, mage.cards.s.SwordsToPlowshares.class, NON_FULL_USE_VARIOUS)); - cards.add(new SetCardInfo("Grim Tutor", 365, Rarity.RARE, mage.cards.g.GrimTutor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Grim Tutor", 365, Rarity.MYTHIC, mage.cards.g.GrimTutor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Blood Moon", 366, Rarity.RARE, mage.cards.b.BloodMoon.class)); cards.add(new SetCardInfo("Cut // Ribbons", 367, Rarity.RARE, mage.cards.c.CutRibbons.class)); cards.add(new SetCardInfo("Teferi's Puzzle Box", 368, Rarity.RARE, mage.cards.t.TeferisPuzzleBox.class)); From 47e2bf9960b64d7f9a58df9a0769105e76e2e88a Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:43:11 -0400 Subject: [PATCH 14/44] fix #13430 (The Aetherspark) presumably this was the problem --- Mage.Sets/src/mage/cards/t/TheAetherspark.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/t/TheAetherspark.java b/Mage.Sets/src/mage/cards/t/TheAetherspark.java index c85453d48c8..1ede8353291 100644 --- a/Mage.Sets/src/mage/cards/t/TheAetherspark.java +++ b/Mage.Sets/src/mage/cards/t/TheAetherspark.java @@ -100,7 +100,7 @@ class TheAethersparkEffect extends RestrictionEffect { @Override public boolean applies(Permanent permanent, Ability source, Game game) { return Optional - .ofNullable(source.getControllerId()) + .ofNullable(source.getSourceId()) .map(game::getPermanent) .map(Permanent::getAttachedTo) .map(game::getPermanent) From 23859c59b3a6f5546d439b5913fa9329ab4350aa Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Tue, 11 Mar 2025 01:20:06 -0400 Subject: [PATCH 15/44] fix Macabre Reconstruction condition --- .../mage/cards/m/MacabreReconstruction.java | 72 ++----------------- 1 file changed, 4 insertions(+), 68 deletions(-) diff --git a/Mage.Sets/src/mage/cards/m/MacabreReconstruction.java b/Mage.Sets/src/mage/cards/m/MacabreReconstruction.java index cf4dc3a5f4d..8463d84dc66 100644 --- a/Mage.Sets/src/mage/cards/m/MacabreReconstruction.java +++ b/Mage.Sets/src/mage/cards/m/MacabreReconstruction.java @@ -1,26 +1,17 @@ package mage.cards.m; -import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CreaturePutInYourGraveyardCondition; import mage.abilities.effects.common.ReturnFromGraveyardToHandTargetEffect; import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; -import mage.abilities.hint.ConditionHint; -import mage.abilities.hint.Hint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.WatcherScope; import mage.constants.Zone; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; import mage.target.common.TargetCardInYourGraveyard; -import mage.watchers.Watcher; +import mage.watchers.common.CreaturePutIntoGraveyardWatcher; -import java.util.HashSet; -import java.util.Set; import java.util.UUID; /** @@ -33,8 +24,8 @@ public final class MacabreReconstruction extends CardImpl { // This spell costs {2} less to cast if a creature card was put into your graveyard from anywhere this turn. this.addAbility(new SimpleStaticAbility( - Zone.ALL, new SpellCostReductionSourceEffect(2, MacabreReconstructionCondition.instance) - ).setRuleAtTheTop(true).addHint(MacabreReconstructionCondition.getHint()), new MacabreReconstructionWatcher()); + Zone.ALL, new SpellCostReductionSourceEffect(2, CreaturePutInYourGraveyardCondition.instance) + ).setRuleAtTheTop(true).addHint(CreaturePutInYourGraveyardCondition.getHint()), new CreaturePutIntoGraveyardWatcher()); // Return up to two target creature cards from your graveyard to your hand. this.getSpellAbility().addEffect(new ReturnFromGraveyardToHandTargetEffect()); @@ -52,58 +43,3 @@ public final class MacabreReconstruction extends CardImpl { return new MacabreReconstruction(this); } } - -enum MacabreReconstructionCondition implements Condition { - instance; - private static final Hint hint = new ConditionHint( - instance, "A card was put into your graveyard this turn" - ); - - public static Hint getHint() { - return hint; - } - - @Override - public boolean apply(Game game, Ability source) { - return MacabreReconstructionWatcher.checkPlayer(source.getControllerId(), game); - } - - @Override - public String toString() { - return "a creature card was put into your graveyard from anywhere this turn"; - } -} - -class MacabreReconstructionWatcher extends Watcher { - - private final Set set = new HashSet<>(); - - MacabreReconstructionWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (event.getType() != GameEvent.EventType.ZONE_CHANGE) { - return; - } - ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - if (Zone.GRAVEYARD.match(zEvent.getToZone())) { - set.add(game.getOwnerId(zEvent.getTargetId())); - } - } - - @Override - public void reset() { - super.reset(); - set.clear(); - } - - static boolean checkPlayer(UUID playerId, Game game) { - return game - .getState() - .getWatcher(MacabreReconstructionWatcher.class) - .set - .contains(playerId); - } -} From 44839647ca9a3bba3c6a1bd2707c30c0be75e4df Mon Sep 17 00:00:00 2001 From: Steven Knipe Date: Tue, 11 Mar 2025 13:13:59 -0700 Subject: [PATCH 16/44] Fix Sumala Sentry (#13432) - the filter is applied after flipping up --- Mage.Sets/src/mage/cards/s/SumalaSentry.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Mage.Sets/src/mage/cards/s/SumalaSentry.java b/Mage.Sets/src/mage/cards/s/SumalaSentry.java index bb2a0b7d1b1..cc074beda1c 100644 --- a/Mage.Sets/src/mage/cards/s/SumalaSentry.java +++ b/Mage.Sets/src/mage/cards/s/SumalaSentry.java @@ -13,7 +13,6 @@ import mage.constants.SubType; import mage.counters.CounterType; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; -import mage.filter.predicate.card.FaceDownPredicate; import java.util.UUID; @@ -24,10 +23,6 @@ public final class SumalaSentry extends CardImpl { private static final FilterPermanent filter = new FilterControlledPermanent("a face-down permanent you control"); - static { - filter.add(FaceDownPredicate.instance); - } - public SumalaSentry(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{W}"); From 8267d7d7704fd0878861e0070b10422b4f1f0072 Mon Sep 17 00:00:00 2001 From: 4825764518 <100122841+4825764518@users.noreply.github.com> Date: Thu, 13 Mar 2025 01:01:25 -0400 Subject: [PATCH 17/44] [DRC] Implement Priest of the Crossing (#13394) * Implement Priest of the Crossing * Add common dynamic value for "creatures that died under your control this turn" --- Mage.Sets/src/mage/cards/b/BodyCount.java | 38 ++-------------- .../src/mage/cards/c/CallousSellSword.java | 34 ++------------ .../src/mage/cards/p/PriestOfTheCrossing.java | 45 +++++++++++++++++++ Mage.Sets/src/mage/cards/s/SeasonOfLoss.java | 36 ++------------- .../src/mage/sets/AetherdriftCommander.java | 2 + .../common/CreaturesYouControlDiedCount.java | 33 ++++++++++++++ 6 files changed, 90 insertions(+), 98 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/p/PriestOfTheCrossing.java create mode 100644 Mage/src/main/java/mage/abilities/dynamicvalue/common/CreaturesYouControlDiedCount.java diff --git a/Mage.Sets/src/mage/cards/b/BodyCount.java b/Mage.Sets/src/mage/cards/b/BodyCount.java index 604c95394aa..cafd38ccaab 100644 --- a/Mage.Sets/src/mage/cards/b/BodyCount.java +++ b/Mage.Sets/src/mage/cards/b/BodyCount.java @@ -1,9 +1,7 @@ package mage.cards.b; -import mage.abilities.Ability; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.effects.Effect; +import mage.abilities.dynamicvalue.common.CreaturesYouControlDiedCount; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.hint.Hint; import mage.abilities.hint.ValueHint; @@ -11,8 +9,6 @@ import mage.abilities.keyword.SpectacleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.game.Game; -import mage.watchers.common.CreaturesDiedWatcher; import java.util.UUID; @@ -22,14 +18,14 @@ import java.util.UUID; public final class BodyCount extends CardImpl { private static final Hint hint = new ValueHint( - "Creatures that died under your control this turn", BodyCountValue.instance + "Creatures that died under your control this turn", CreaturesYouControlDiedCount.instance ); public BodyCount(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{B}"); // Draw a card for each creature that died under your control this turn. - this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(BodyCountValue.instance)); + this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(CreaturesYouControlDiedCount.instance)); this.getSpellAbility().addHint(hint); // Spectacle {B} @@ -44,30 +40,4 @@ public final class BodyCount extends CardImpl { public BodyCount copy() { return new BodyCount(this); } -} - -enum BodyCountValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - return game.getState() - .getWatcher(CreaturesDiedWatcher.class) - .getAmountOfCreaturesDiedThisTurnByController(sourceAbility.getControllerId()); - } - - @Override - public BodyCountValue copy() { - return this; - } - - @Override - public String getMessage() { - return "creature that died under your control this turn"; - } - - @Override - public String toString() { - return "1"; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/c/CallousSellSword.java b/Mage.Sets/src/mage/cards/c/CallousSellSword.java index ced7dc1aa8e..a727344100e 100644 --- a/Mage.Sets/src/mage/cards/c/CallousSellSword.java +++ b/Mage.Sets/src/mage/cards/c/CallousSellSword.java @@ -3,8 +3,7 @@ package mage.cards.c; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldAbility; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.effects.Effect; +import mage.abilities.dynamicvalue.common.CreaturesYouControlDiedCount; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DamageWithPowerFromOneToAnotherTargetEffect; import mage.abilities.effects.common.counter.AddCountersSourceEffect; @@ -21,7 +20,6 @@ import mage.game.Game; import mage.game.permanent.Permanent; import mage.target.common.TargetAnyTarget; import mage.target.common.TargetControlledCreaturePermanent; -import mage.watchers.common.CreaturesDiedWatcher; import java.util.UUID; @@ -33,7 +31,7 @@ public final class CallousSellSword extends AdventureCard { private static final FilterAnyTarget filterSecondTarget = new FilterAnyTarget("any other target"); private static final Hint hint = new ValueHint( - "Creatures that died under your control this turn", CallousSellSwordValue.instance + "Creatures that died under your control this turn", CreaturesYouControlDiedCount.instance ); public CallousSellSword(UUID ownerId, CardSetInfo setInfo) { @@ -48,7 +46,7 @@ public final class CallousSellSword extends AdventureCard { this.addAbility(new EntersBattlefieldAbility( new AddCountersSourceEffect( CounterType.P1P1.createInstance(0), - CallousSellSwordValue.instance, true + CreaturesYouControlDiedCount.instance, true ).setText("with a +1/+1 counter on it for each creature that died under your control this turn.") ).addHint(hint)); @@ -72,32 +70,6 @@ public final class CallousSellSword extends AdventureCard { } } -enum CallousSellSwordValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - return game.getState() - .getWatcher(CreaturesDiedWatcher.class) - .getAmountOfCreaturesDiedThisTurnByController(sourceAbility.getControllerId()); - } - - @Override - public CallousSellSwordValue copy() { - return this; - } - - @Override - public String getMessage() { - return "creature that died under your control this turn"; - } - - @Override - public String toString() { - return "1"; - } -} - class CallousSellSwordSacrificeFirstTargetEffect extends OneShotEffect { CallousSellSwordSacrificeFirstTargetEffect() { diff --git a/Mage.Sets/src/mage/cards/p/PriestOfTheCrossing.java b/Mage.Sets/src/mage/cards/p/PriestOfTheCrossing.java new file mode 100644 index 00000000000..f35d63dfdad --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PriestOfTheCrossing.java @@ -0,0 +1,45 @@ +package mage.cards.p; + +import mage.MageInt; +import mage.abilities.dynamicvalue.common.CreaturesYouControlDiedCount; +import mage.abilities.effects.common.counter.AddCountersAllEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +public class PriestOfTheCrossing extends CardImpl { + public PriestOfTheCrossing(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.addSubType(SubType.ZOMBIE); + this.addSubType(SubType.BIRD); + this.addSubType(SubType.CLERIC); + + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // At the beginning of each end step, put X +1/+1 counters on each creature you control, where X is the number of creatures that died under your control this turn. + this.addAbility(new BeginningOfEndStepTriggeredAbility( + new AddCountersAllEffect(CounterType.P1P1.createInstance(), CreaturesYouControlDiedCount.instance, StaticFilters.FILTER_CONTROLLED_CREATURE) + .setText("put X +1/+1 counters on each creature you control, where X is the number of creatures that died under your control this turn"))); + } + + public PriestOfTheCrossing(PriestOfTheCrossing card) { + super(card); + } + + @Override + public PriestOfTheCrossing copy() { + return new PriestOfTheCrossing(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SeasonOfLoss.java b/Mage.Sets/src/mage/cards/s/SeasonOfLoss.java index edb864fad0b..552af8c9acc 100644 --- a/Mage.Sets/src/mage/cards/s/SeasonOfLoss.java +++ b/Mage.Sets/src/mage/cards/s/SeasonOfLoss.java @@ -1,10 +1,9 @@ package mage.cards.s; -import mage.abilities.Ability; import mage.abilities.Mode; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; -import mage.abilities.effects.Effect; +import mage.abilities.dynamicvalue.common.CreaturesYouControlDiedCount; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.LoseLifeOpponentsEffect; import mage.abilities.effects.common.SacrificeAllEffect; @@ -13,8 +12,6 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.StaticFilters; -import mage.game.Game; -import mage.watchers.common.CreaturesDiedWatcher; import java.util.UUID; @@ -37,7 +34,7 @@ public final class SeasonOfLoss extends CardImpl { this.getSpellAbility().getModes().getMode().withPawPrintValue(1); // {P}{P} -- Draw a card for each creature you controlled that died this turn. - Mode mode2 = new Mode(new DrawCardSourceControllerEffect(SeasonOfLossValue.instance)); + Mode mode2 = new Mode(new DrawCardSourceControllerEffect(CreaturesYouControlDiedCount.instance)); this.getSpellAbility().addMode(mode2.withPawPrintValue(2)); // {P}{P}{P} -- Each opponent loses X life, where X is the number of creature cards in your graveyard. @@ -56,31 +53,4 @@ public final class SeasonOfLoss extends CardImpl { public SeasonOfLoss copy() { return new SeasonOfLoss(this); } -} - -// Based on CallousSellSwordValue -enum SeasonOfLossValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - return game.getState() - .getWatcher(CreaturesDiedWatcher.class) - .getAmountOfCreaturesDiedThisTurnByController(sourceAbility.getControllerId()); - } - - @Override - public SeasonOfLossValue copy() { - return this; - } - - @Override - public String getMessage() { - return "creature that died under your control this turn"; - } - - @Override - public String toString() { - return "1"; - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/AetherdriftCommander.java b/Mage.Sets/src/mage/sets/AetherdriftCommander.java index e7c4cd86c29..10328d49a35 100644 --- a/Mage.Sets/src/mage/sets/AetherdriftCommander.java +++ b/Mage.Sets/src/mage/sets/AetherdriftCommander.java @@ -125,6 +125,8 @@ public final class AetherdriftCommander extends ExpansionSet { cards.add(new SetCardInfo("Pia and Kiran Nalaar", 105, Rarity.RARE, mage.cards.p.PiaAndKiranNalaar.class)); cards.add(new SetCardInfo("Plague Belcher", 97, Rarity.RARE, mage.cards.p.PlagueBelcher.class)); cards.add(new SetCardInfo("Prairie Stream", 167, Rarity.RARE, mage.cards.p.PrairieStream.class)); + cards.add(new SetCardInfo("Priest of the Crossing", 6, Rarity.RARE, mage.cards.p.PriestOfTheCrossing.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Priest of the Crossing", 22, Rarity.RARE, mage.cards.p.PriestOfTheCrossing.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Prophet of the Scarab", 9, Rarity.RARE, mage.cards.p.ProphetOfTheScarab.class)); cards.add(new SetCardInfo("Pull from Tomorrow", 81, Rarity.RARE, mage.cards.p.PullFromTomorrow.class)); cards.add(new SetCardInfo("Reality Shift", 39, Rarity.UNCOMMON, mage.cards.r.RealityShift.class)); diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/CreaturesYouControlDiedCount.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CreaturesYouControlDiedCount.java new file mode 100644 index 00000000000..8b17ff9e9ac --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/CreaturesYouControlDiedCount.java @@ -0,0 +1,33 @@ +package mage.abilities.dynamicvalue.common; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.game.Game; +import mage.watchers.common.CreaturesDiedWatcher; + +public enum CreaturesYouControlDiedCount implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return game.getState() + .getWatcher(CreaturesDiedWatcher.class) + .getAmountOfCreaturesDiedThisTurnByController(sourceAbility.getControllerId()); + } + + @Override + public CreaturesYouControlDiedCount copy() { + return this; + } + + @Override + public String getMessage() { + return "creature that died under your control this turn"; + } + + @Override + public String toString() { + return "1"; + } +} From cd8cb6afe578236bd8f2ed60a021a650a17f73b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Krist=C3=B3f?= <20043803+balazskristof@users.noreply.github.com> Date: Fri, 14 Mar 2025 01:51:59 +0100 Subject: [PATCH 18/44] [ACR] Implement Overpowering Attack (#13398) * [ACR] Implement Overpowering Attack * Replaced UntapAllThatAttackedEffect with filtered UntapAllEffect * Add MyTurnCondition to Overpowering Attack --- .../src/mage/cards/f/FuryOfTheHorde.java | 9 ++-- .../src/mage/cards/o/OverpoweringAttack.java | 48 +++++++++++++++++++ .../src/mage/cards/r/RelentlessAssault.java | 13 +++-- .../src/mage/cards/w/WavesOfAggression.java | 14 ++++-- Mage.Sets/src/mage/sets/AssassinsCreed.java | 4 +- .../common/UntapAllThatAttackedEffect.java | 48 ------------------- .../permanent/AttackedThisTurnPredicate.java | 25 ++++++++++ 7 files changed, 102 insertions(+), 59 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/o/OverpoweringAttack.java delete mode 100644 Mage/src/main/java/mage/abilities/effects/common/UntapAllThatAttackedEffect.java create mode 100644 Mage/src/main/java/mage/filter/predicate/permanent/AttackedThisTurnPredicate.java diff --git a/Mage.Sets/src/mage/cards/f/FuryOfTheHorde.java b/Mage.Sets/src/mage/cards/f/FuryOfTheHorde.java index 7be92336414..3679df4841b 100644 --- a/Mage.Sets/src/mage/cards/f/FuryOfTheHorde.java +++ b/Mage.Sets/src/mage/cards/f/FuryOfTheHorde.java @@ -6,14 +6,15 @@ import mage.ObjectColor; import mage.abilities.costs.AlternativeCostSourceAbility; import mage.abilities.costs.common.ExileFromHandCost; import mage.abilities.effects.common.AddCombatAndMainPhaseEffect; -import mage.abilities.effects.common.UntapAllThatAttackedEffect; +import mage.abilities.effects.common.UntapAllEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; +import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.ColorPredicate; +import mage.filter.predicate.permanent.AttackedThisTurnPredicate; import mage.target.common.TargetCardInHand; -import mage.watchers.common.AttackedThisTurnWatcher; /** * @@ -22,9 +23,11 @@ import mage.watchers.common.AttackedThisTurnWatcher; public final class FuryOfTheHorde extends CardImpl { private static final FilterCard filter = new FilterCard("red cards"); + private static final FilterCreaturePermanent filter2 = new FilterCreaturePermanent("creatures that attacked this turn"); static { filter.add(new ColorPredicate(ObjectColor.RED)); + filter2.add(AttackedThisTurnPredicate.instance); } public FuryOfTheHorde(UUID ownerId, CardSetInfo setInfo) { @@ -34,7 +37,7 @@ public final class FuryOfTheHorde extends CardImpl { this.addAbility(new AlternativeCostSourceAbility(new ExileFromHandCost(new TargetCardInHand(2, filter)))); // Untap all creatures that attacked this turn. After this main phase, there is an additional combat phase followed by an additional main phase. - this.getSpellAbility().addEffect(new UntapAllThatAttackedEffect()); + this.getSpellAbility().addEffect(new UntapAllEffect(filter2)); this.getSpellAbility().addEffect(new AddCombatAndMainPhaseEffect()); } diff --git a/Mage.Sets/src/mage/cards/o/OverpoweringAttack.java b/Mage.Sets/src/mage/cards/o/OverpoweringAttack.java new file mode 100644 index 00000000000..a5e8544e9b1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OverpoweringAttack.java @@ -0,0 +1,48 @@ +package mage.cards.o; + +import java.util.UUID; + +import mage.abilities.condition.common.MyTurnCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.AddCombatAndMainPhaseEffect; +import mage.abilities.effects.common.UntapAllControllerEffect; +import mage.abilities.keyword.FreerunningAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.permanent.AttackedThisTurnPredicate; + +/** + * + * @author balazskristof + */ +public final class OverpoweringAttack extends CardImpl { + + private static final FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent(); + + static { + filter.add(AttackedThisTurnPredicate.instance); + } + + public OverpoweringAttack(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{R}{R}"); + + + // Freerunning {2}{R} + this.addAbility(new FreerunningAbility("{2}{R}")); + + // Untap all creatures you control that attacked this turn. If it's your main phase, there is an additional combat phase after this phase, followed by an additional main phase. + this.getSpellAbility().addEffect(new UntapAllControllerEffect(filter, "untap all creatures you control that attacked this turn")); + this.getSpellAbility().addEffect(new ConditionalOneShotEffect(new AddCombatAndMainPhaseEffect(), MyTurnCondition.instance, "If it's your main phase, there is an additional combat phase after this phase, followed by an additional main phase.")); + } + + private OverpoweringAttack(final OverpoweringAttack card) { + super(card); + } + + @Override + public OverpoweringAttack copy() { + return new OverpoweringAttack(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RelentlessAssault.java b/Mage.Sets/src/mage/cards/r/RelentlessAssault.java index f4ed42f3842..3653960e528 100644 --- a/Mage.Sets/src/mage/cards/r/RelentlessAssault.java +++ b/Mage.Sets/src/mage/cards/r/RelentlessAssault.java @@ -3,11 +3,12 @@ package mage.cards.r; import java.util.UUID; import mage.abilities.effects.common.AddCombatAndMainPhaseEffect; -import mage.abilities.effects.common.UntapAllThatAttackedEffect; +import mage.abilities.effects.common.UntapAllEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.watchers.common.AttackedThisTurnWatcher; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.AttackedThisTurnPredicate; /** * @@ -15,11 +16,17 @@ import mage.watchers.common.AttackedThisTurnWatcher; */ public final class RelentlessAssault extends CardImpl { + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creatures that attacked this turn"); + + static { + filter.add(AttackedThisTurnPredicate.instance); + } + public RelentlessAssault(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{2}{R}{R}"); // Untap all creatures that attacked this turn. After this main phase, there is an additional combat phase followed by an additional main phase. - this.getSpellAbility().addEffect(new UntapAllThatAttackedEffect()); + this.getSpellAbility().addEffect(new UntapAllEffect(filter)); this.getSpellAbility().addEffect(new AddCombatAndMainPhaseEffect()); } diff --git a/Mage.Sets/src/mage/cards/w/WavesOfAggression.java b/Mage.Sets/src/mage/cards/w/WavesOfAggression.java index 96a8dac9fec..42c836e8edd 100644 --- a/Mage.Sets/src/mage/cards/w/WavesOfAggression.java +++ b/Mage.Sets/src/mage/cards/w/WavesOfAggression.java @@ -2,13 +2,15 @@ package mage.cards.w; import java.util.UUID; + import mage.abilities.effects.common.AddCombatAndMainPhaseEffect; -import mage.abilities.effects.common.UntapAllThatAttackedEffect; +import mage.abilities.effects.common.UntapAllEffect; import mage.abilities.keyword.RetraceAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.watchers.common.AttackedThisTurnWatcher; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.AttackedThisTurnPredicate; /** * @@ -16,11 +18,17 @@ import mage.watchers.common.AttackedThisTurnWatcher; */ public final class WavesOfAggression extends CardImpl { + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creatures that attacked this turn"); + + static { + filter.add(AttackedThisTurnPredicate.instance); + } + public WavesOfAggression(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{3}{R/W}{R/W}"); // Untap all creatures that attacked this turn. After this main phase, there is an additional combat phase followed by an additional main phase. - this.getSpellAbility().addEffect(new UntapAllThatAttackedEffect()); + this.getSpellAbility().addEffect(new UntapAllEffect(filter)); this.getSpellAbility().addEffect(new AddCombatAndMainPhaseEffect()); // Retrace this.addAbility(new RetraceAbility(this)); diff --git a/Mage.Sets/src/mage/sets/AssassinsCreed.java b/Mage.Sets/src/mage/sets/AssassinsCreed.java index 5897aac8c14..63b30bf8a39 100644 --- a/Mage.Sets/src/mage/sets/AssassinsCreed.java +++ b/Mage.Sets/src/mage/sets/AssassinsCreed.java @@ -221,8 +221,8 @@ public final class AssassinsCreed extends ExpansionSet { cards.add(new SetCardInfo("Murder", 92, Rarity.UNCOMMON, mage.cards.m.Murder.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Nurturing Peatland", 114, Rarity.RARE, mage.cards.n.NurturingPeatland.class)); cards.add(new SetCardInfo("Origin of the Hidden Ones", 36, Rarity.UNCOMMON, mage.cards.o.OriginOfTheHiddenOnes.class)); - //cards.add(new SetCardInfo("Overpowering Attack", 218, Rarity.UNCOMMON, mage.cards.o.OverpoweringAttack.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Overpowering Attack", 37, Rarity.UNCOMMON, mage.cards.o.OverpoweringAttack.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Overpowering Attack", 218, Rarity.UNCOMMON, mage.cards.o.OverpoweringAttack.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Overpowering Attack", 37, Rarity.UNCOMMON, mage.cards.o.OverpoweringAttack.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Palazzo Archers", 222, Rarity.UNCOMMON, mage.cards.p.PalazzoArchers.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Palazzo Archers", 42, Rarity.UNCOMMON, mage.cards.p.PalazzoArchers.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Path to Exile", 178, Rarity.UNCOMMON, mage.cards.p.PathToExile.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/UntapAllThatAttackedEffect.java b/Mage/src/main/java/mage/abilities/effects/common/UntapAllThatAttackedEffect.java deleted file mode 100644 index 3070c20ed70..00000000000 --- a/Mage/src/main/java/mage/abilities/effects/common/UntapAllThatAttackedEffect.java +++ /dev/null @@ -1,48 +0,0 @@ -package mage.abilities.effects.common; - -import mage.MageObjectReference; -import mage.abilities.Ability; -import mage.abilities.effects.OneShotEffect; -import mage.constants.Outcome; -import mage.game.Game; -import mage.game.permanent.Permanent; -import mage.watchers.common.AttackedThisTurnWatcher; - -import java.util.Set; - -/** - * @author LevelX2 - */ -public class UntapAllThatAttackedEffect extends OneShotEffect { - - public UntapAllThatAttackedEffect() { - super(Outcome.Benefit); - staticText = "Untap all creatures that attacked this turn"; - } - - protected UntapAllThatAttackedEffect(final UntapAllThatAttackedEffect effect) { - super(effect); - } - - @Override - public UntapAllThatAttackedEffect copy() { - return new UntapAllThatAttackedEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - AttackedThisTurnWatcher watcher = game.getState().getWatcher(AttackedThisTurnWatcher.class); - if (watcher != null) { - Set attackedThisTurn = watcher.getAttackedThisTurnCreatures(); - for (MageObjectReference mor : attackedThisTurn) { - Permanent permanent = mor.getPermanent(game); - if (permanent != null && permanent.isCreature(game)) { - permanent.untap(game); - } - } - return true; - } - return false; - } - -} diff --git a/Mage/src/main/java/mage/filter/predicate/permanent/AttackedThisTurnPredicate.java b/Mage/src/main/java/mage/filter/predicate/permanent/AttackedThisTurnPredicate.java new file mode 100644 index 00000000000..6ad6e8aadb6 --- /dev/null +++ b/Mage/src/main/java/mage/filter/predicate/permanent/AttackedThisTurnPredicate.java @@ -0,0 +1,25 @@ +package mage.filter.predicate.permanent; + +import mage.filter.predicate.Predicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.watchers.common.AttackedThisTurnWatcher; + +/** + * + * @author balazskristof + */ +public enum AttackedThisTurnPredicate implements Predicate { + instance; + + @Override + public boolean apply(Permanent input, Game game) { + AttackedThisTurnWatcher watcher = game.getState().getWatcher(AttackedThisTurnWatcher.class); + return watcher != null && watcher.checkIfAttacked(input, game); + } + + @Override + public String toString() { + return "attacked this turn"; + } +} From 9aaad5193f668ebb997a402d56e2e176485a8a1c Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Thu, 13 Mar 2025 22:24:26 -0500 Subject: [PATCH 19/44] [DFT] Implement Elvish Refueler (#13392) --- .../src/mage/cards/e/ElvishRefueler.java | 151 ++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 1 + .../cards/single/dft/ElvishRefuelerTest.java | 60 +++++++ .../abilities/keyword/ExhaustAbility.java | 16 ++ .../mage/constants/AsThoughEffectType.java | 5 +- 5 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/e/ElvishRefueler.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/dft/ElvishRefuelerTest.java diff --git a/Mage.Sets/src/mage/cards/e/ElvishRefueler.java b/Mage.Sets/src/mage/cards/e/ElvishRefueler.java new file mode 100644 index 00000000000..e184cc92e0c --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/ElvishRefueler.java @@ -0,0 +1,151 @@ +package mage.cards.e; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.constants.*; +import mage.abilities.keyword.ExhaustAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.stack.StackObject; +import mage.watchers.Watcher; + +/** + * + * @author Jmlundeen + */ +public final class ElvishRefueler extends CardImpl { + private static final FilterPermanent filter = new FilterPermanent(); + private static final Hint hint = new ConditionHint(ActivatedExhaustCondition.instance, + "You haven't activated an exhaust ability this turn", + null, + "You have activated an exhaust ability this turn", + null, + true); + + static { + filter.add(TargetController.YOU.getControllerPredicate()); + } + + public ElvishRefueler(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.ELF); + this.subtype.add(SubType.DRUID); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // During your turn, as long as you haven't activated an exhaust ability this turn, you may activate exhaust abilities as though they haven't been activated. + this.addAbility(new SimpleStaticAbility(new ConditionalAsThoughEffect( + new ElvishRefuelerEffect(filter), ActivatedExhaustCondition.instance) + .setText("During your turn, as long as you haven't activated an exhaust ability this turn, " + + "you may activate exhaust abilities as though they haven't been activated") + ).addHint(hint), new ElvishRefuelerWatcher()); + + // Exhaust -- {1}{G}: Put a +1/+1 counter on this creature. + this.addAbility(new ExhaustAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance()), new ManaCostsImpl<>("{1}{G}"))); + + } + + private ElvishRefueler(final ElvishRefueler card) { + super(card); + } + + @Override + public ElvishRefueler copy() { + return new ElvishRefueler(this); + } + +} + +enum ActivatedExhaustCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + ElvishRefuelerWatcher watcher = game.getState().getWatcher(ElvishRefuelerWatcher.class); + return watcher != null && !watcher.checkActivatedExhaust(source.getControllerId()); + } +} + +class ElvishRefuelerWatcher extends Watcher { + + private final Map activatedExhaustAbilities = new HashMap<>(); + + public ElvishRefuelerWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.ACTIVATED_ABILITY) { + return; + } + StackObject stackObject = game.getStack().getStackObject(event.getSourceId()); + if (stackObject != null && stackObject.getStackAbility() instanceof ExhaustAbility) { + activatedExhaustAbilities.put(event.getPlayerId(), true); + } + } + + @Override + public void reset() { + super.reset(); + activatedExhaustAbilities.clear(); + } + + public boolean checkActivatedExhaust(UUID playerId) { + return activatedExhaustAbilities.getOrDefault(playerId, false); + } +} + +class ElvishRefuelerEffect extends AsThoughEffectImpl { + private final FilterPermanent filter; + + ElvishRefuelerEffect(FilterPermanent filter) { + super(AsThoughEffectType.ALLOW_EXHAUST_PER_TURN, Duration.WhileOnBattlefield, Outcome.Benefit); + this.filter = filter; + staticText = "During your turn, as long as you haven't activated an exhaust ability this turn, " + + "you may activate exhaust abilities as though they haven't been activated"; + } + + ElvishRefuelerEffect(final ElvishRefuelerEffect effect) { + super(effect); + this.filter = effect.filter; + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public ElvishRefuelerEffect copy() { + return new ElvishRefuelerEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + ElvishRefuelerWatcher watcher = game.getState().getWatcher(ElvishRefuelerWatcher.class); + if (!game.isActivePlayer(affectedControllerId) || watcher == null || watcher.checkActivatedExhaust(affectedControllerId)) { + return false; + } + Permanent permanent = game.getPermanent(objectId); + return filter.match(permanent, source.getControllerId(), source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index d76124abaab..5e2dbd77c71 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -107,6 +107,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Dune Drifter", 200, Rarity.UNCOMMON, mage.cards.d.DuneDrifter.class)); cards.add(new SetCardInfo("Dynamite Diver", 123, Rarity.COMMON, mage.cards.d.DynamiteDiver.class)); cards.add(new SetCardInfo("Earthrumbler", 160, Rarity.UNCOMMON, mage.cards.e.Earthrumbler.class)); + cards.add(new SetCardInfo("Elvish Refueler", 161, Rarity.UNCOMMON, mage.cards.e.ElvishRefueler.class)); cards.add(new SetCardInfo("Embalmed Ascendant", 201, Rarity.UNCOMMON, mage.cards.e.EmbalmedAscendant.class)); cards.add(new SetCardInfo("Endrider Catalyzer", 124, Rarity.COMMON, mage.cards.e.EndriderCatalyzer.class)); cards.add(new SetCardInfo("Endrider Spikespitter", 125, Rarity.UNCOMMON, mage.cards.e.EndriderSpikespitter.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/ElvishRefuelerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/ElvishRefuelerTest.java new file mode 100644 index 00000000000..56c9cde2498 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/ElvishRefuelerTest.java @@ -0,0 +1,60 @@ +package org.mage.test.cards.single.dft; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author jimga150 + */ +public class ElvishRefuelerTest extends CardTestPlayerBase { + + @Test + public void testExhaustPrevention() { + // During your turn, as long as you haven’t activated an exhaust ability this turn, + // you may activate exhaust abilities as though they haven’t been activated. + String refueler = "Elvish Refueler"; + addCard(Zone.BATTLEFIELD, playerA, refueler); + addCard(Zone.HAND, playerA, refueler); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 7); + + // Should be able to activate refueler's exhaust ability + checkPlayableAbility("Should be able to activate exhaust", + 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Exhaust", true); + // Activate refueler's exhaust ability + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Exhaust"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + // Should not be able to activate refueler's exhaust ability + checkPlayableAbility("Should not be able to activate exhaust after activating", + 1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Exhaust", false); + // Casting a second refueler should not allow activating the exhaust ability + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, refueler); + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + // Activating second refueler's exhaust ability + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Exhaust"); + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + // Should be no exhaust left + checkPlayableAbility("All exhausts should be used from both refuelers", + 1, PhaseStep.END_TURN, playerA, "Exhaust", false); + + // Confirm on opponent's turn that exhaust is still not available + checkPlayableAbility("Opponent's turn, effect should not apply", + 2, PhaseStep.END_TURN, playerA, "Exhaust", false); + + // Should be able to activate refueler's exhaust ability on next turn + checkPlayableAbility("Should be able to activate exhaust on our next turn", + 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Exhaust", true); + // Activate refueler's exhaust ability + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Exhaust"); + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); + // Only one exhaust should have been available + checkPlayableAbility("Already activated exhaust, should not be able to activate again", + 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Exhaust", false); + setStopAt(3, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + } + + +} diff --git a/Mage/src/main/java/mage/abilities/keyword/ExhaustAbility.java b/Mage/src/main/java/mage/abilities/keyword/ExhaustAbility.java index 4b325da69ed..7528788e1b1 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ExhaustAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ExhaustAbility.java @@ -3,7 +3,9 @@ package mage.abilities.keyword; import mage.abilities.ActivatedAbilityImpl; import mage.abilities.costs.Cost; import mage.abilities.effects.Effect; +import mage.constants.AsThoughEffectType; import mage.constants.Zone; +import mage.game.Game; /** * @author TheElk801 @@ -24,6 +26,20 @@ public class ExhaustAbility extends ActivatedAbilityImpl { return new ExhaustAbility(this); } + @Override + public boolean hasMoreActivationsThisTurn(Game game) { + ActivationInfo info = getActivationInfo(game); + if (info != null && info.totalActivations >= maxActivationsPerGame) { + boolean canActivate = !game.getContinuousEffects() + .asThough(sourceId, AsThoughEffectType.ALLOW_EXHAUST_PER_TURN, this, controllerId, game) + .isEmpty(); + if (canActivate) { + return true; + } + } + return super.hasMoreActivationsThisTurn(game); + } + @Override public String getRule() { return "Exhaust — " + super.getRule() + " (Activate each exhaust ability only once.)"; diff --git a/Mage/src/main/java/mage/constants/AsThoughEffectType.java b/Mage/src/main/java/mage/constants/AsThoughEffectType.java index e6b6b5e8c02..03fefec570f 100644 --- a/Mage/src/main/java/mage/constants/AsThoughEffectType.java +++ b/Mage/src/main/java/mage/constants/AsThoughEffectType.java @@ -59,7 +59,10 @@ public enum AsThoughEffectType { // // ALLOW_FORETELL_ANYTIME: // For Cosmos Charger effect - ALLOW_FORETELL_ANYTIME; + ALLOW_FORETELL_ANYTIME, + // ALLOW_EXHAUST_ACTIVE_ABILITY: + // Elvish Refueler effect allows Exhaust on your turn as though it hasn't been activated + ALLOW_EXHAUST_PER_TURN(true, false); private final boolean needAffectedAbility; // mark what AsThough check must be called for specific ability, not full object (example: spell check) private final boolean needPlayCardAbility; // mark what AsThough check must be called for play/cast abilities From ba397f32788bf5e34de037b5b36cd7a459bc0be1 Mon Sep 17 00:00:00 2001 From: padfoothelix Date: Sat, 15 Mar 2025 01:44:52 +0100 Subject: [PATCH 20/44] [WHO] Implement The Sound Of Drums (#13372) --- .../src/mage/cards/t/TheSoundOfDrums.java | 104 ++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 8 +- 2 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TheSoundOfDrums.java diff --git a/Mage.Sets/src/mage/cards/t/TheSoundOfDrums.java b/Mage.Sets/src/mage/cards/t/TheSoundOfDrums.java new file mode 100644 index 00000000000..cdf40164080 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheSoundOfDrums.java @@ -0,0 +1,104 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.ReturnSourceFromGraveyardToHandEffect; +import mage.abilities.effects.common.combat.GoadAttachedEffect; +import mage.abilities.effects.common.AttachEffect; +import mage.abilities.keyword.EnchantAbility; +import mage.target.common.TargetCreaturePermanent; +import mage.target.TargetPermanent; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.events.DamageEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; + +import mage.util.CardUtil; +import java.util.UUID; + +/** + * + * @author padfoot + */ +public final class TheSoundOfDrums extends CardImpl { + + public TheSoundOfDrums(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{R}"); + + this.subtype.add(SubType.AURA); + + // Enchant creature + TargetPermanent auraTarget = new TargetCreaturePermanent(); + this.getSpellAbility().addTarget(auraTarget); + this.getSpellAbility().addEffect(new AttachEffect(Outcome.BoostCreature)); + this.addAbility(new EnchantAbility(auraTarget)); + + // Enchanted creature is goaded. + this.addAbility(new SimpleStaticAbility(new GoadAttachedEffect().setText("enchanted creature is goaded"))); + // If enchanted creature would deal combat damage to a permanent or player, it deals double that damage instead. + this.addAbility(new SimpleStaticAbility(new TheSoundOfDrumsEffect())); + // {2}{R}: Return The Sound of Drums from your graveyard to your hand. + this.addAbility(new SimpleActivatedAbility( + Zone.GRAVEYARD, + new ReturnSourceFromGraveyardToHandEffect(), + new ManaCostsImpl<>("{2}{R}") + )); + } + + private TheSoundOfDrums(final TheSoundOfDrums card) { + super(card); + } + + @Override + public TheSoundOfDrums copy() { + return new TheSoundOfDrums(this); + } +} + +class TheSoundOfDrumsEffect extends ReplacementEffectImpl { + + TheSoundOfDrumsEffect() { + super(Duration.WhileOnBattlefield, Outcome.Damage); + staticText = "If enchanted creature would deal combat damage to a permanent or player, it deals double that damage instead"; + } + + private TheSoundOfDrumsEffect(final TheSoundOfDrumsEffect effect) { + super(effect); + } + + @Override + public TheSoundOfDrumsEffect copy() { + return new TheSoundOfDrumsEffect(this); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + event.setAmount(CardUtil.overflowMultiply(event.getAmount(), 2)); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + switch (event.getType()) { + case DAMAGE_PLAYER: + case DAMAGE_PERMANENT: + return true; + default: + return false; + } + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + Permanent enchantment = game.getPermanent(source.getSourceId()); + return enchantment != null + && ((DamageEvent) event).isCombatDamage() + && enchantment.isAttachedTo(event.getSourceId()); + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index 22e233305a0..cc0497825f0 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -1031,10 +1031,10 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("The Sixth Doctor", 443, Rarity.RARE, mage.cards.t.TheSixthDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Sixth Doctor", 557, Rarity.RARE, mage.cards.t.TheSixthDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Sixth Doctor", 764, Rarity.RARE, mage.cards.t.TheSixthDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The Sound of Drums", 391, Rarity.RARE, mage.cards.t.TheSoundOfDrums.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The Sound of Drums", 702, Rarity.RARE, mage.cards.t.TheSoundOfDrums.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The Sound of Drums", 97, Rarity.RARE, mage.cards.t.TheSoundOfDrums.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The Sound of Drums", 982, Rarity.RARE, mage.cards.t.TheSoundOfDrums.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Sound of Drums", 391, Rarity.RARE, mage.cards.t.TheSoundOfDrums.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Sound of Drums", 702, Rarity.RARE, mage.cards.t.TheSoundOfDrums.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Sound of Drums", 97, Rarity.RARE, mage.cards.t.TheSoundOfDrums.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Sound of Drums", 982, Rarity.RARE, mage.cards.t.TheSoundOfDrums.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Tenth Doctor", "561z", Rarity.MYTHIC, mage.cards.t.TheTenthDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Tenth Doctor", 1037, Rarity.MYTHIC, mage.cards.t.TheTenthDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Tenth Doctor", 1152, Rarity.MYTHIC, mage.cards.t.TheTenthDoctor.class, NON_FULL_USE_VARIOUS)); From a9bffe4d25a6925a9c36c98e3cc3ea2d56b62eab Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Fri, 14 Mar 2025 20:45:15 -0400 Subject: [PATCH 21/44] Fix missing ProcessAction between the last mana being paid and the spell being cast. (#13396) This is necessary so that, among other things, the Rain of Riches watcher can see that the last mana of a spell was a Treasure before the spell is cast - at which point it needs to already have Cascade. --- .../java/org/mage/test/cards/single/ncc/RainOfRichesTest.java | 3 --- Mage/src/main/java/mage/players/PlayerImpl.java | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java index 2a65b1976db..d798d2dd873 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java @@ -2,7 +2,6 @@ package org.mage.test.cards.single.ncc; import mage.constants.PhaseStep; import mage.constants.Zone; -import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -63,7 +62,6 @@ public class RainOfRichesTest extends CardTestPlayerBase { } @Test - @Ignore("Does not work until actions are processed between the last mana being paid and the spell being cast.") public void test_Cast_Two_Using_Treasures() { setStrictChooseMode(true); skipInitShuffling(); @@ -91,7 +89,6 @@ public class RainOfRichesTest extends CardTestPlayerBase { } @Test - @Ignore("Does not work until actions are processed between the last mana being paid and the spell being cast.") public void test_Cast_SomethingElse_Then_Cast_Using_Treasure() { setStrictChooseMode(true); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 4db86272845..a6f3ac1dd88 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1346,6 +1346,7 @@ public abstract class PlayerImpl implements Player, Serializable { castEvent.setZone(fromZone); game.fireEvent(castEvent); if (spell.activate(game, allowedIdentifiers, noMana)) { + game.processAction(); GameEvent castedEvent = GameEvent.getEvent(GameEvent.EventType.SPELL_CAST, ability.getId(), ability, playerId, approvingObject); castedEvent.setZone(fromZone); From d5c194e388d79693b254d9170174ff125546346a Mon Sep 17 00:00:00 2001 From: padfoothelix Date: Sat, 15 Mar 2025 01:45:25 +0100 Subject: [PATCH 22/44] Implement The Night Of The Doctor (#13416) --- .../src/mage/cards/t/TheNightOfTheDoctor.java | 110 ++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 4 +- 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TheNightOfTheDoctor.java diff --git a/Mage.Sets/src/mage/cards/t/TheNightOfTheDoctor.java b/Mage.Sets/src/mage/cards/t/TheNightOfTheDoctor.java new file mode 100644 index 00000000000..0a3d98cceb3 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheNightOfTheDoctor.java @@ -0,0 +1,110 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.common.SagaAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DestroyAllEffect; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; +import mage.constants.*; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.StaticFilters; +import mage.players.Player; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.*; +/** + * + * @author padfoothelix + */ +public final class TheNightOfTheDoctor extends CardImpl { + + private static final FilterCard filter = new FilterCreatureCard("legendary creature card"); + + static { + filter.add(SuperType.LEGENDARY.getPredicate()); + } + + public TheNightOfTheDoctor(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{W}{W}"); + + this.subtype.add(SubType.SAGA); + + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after II.) + SagaAbility sagaAbility = new SagaAbility(this, SagaChapter.CHAPTER_II); + + // I -- Destroy all creatures. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I, new DestroyAllEffect(StaticFilters.FILTER_PERMANENT_CREATURES)); + + // II -- Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_II, new TheNightOfTheDoctorEffect(), new TargetCardInYourGraveyard(filter)); + + this.addAbility(sagaAbility); + + } + + private TheNightOfTheDoctor(final TheNightOfTheDoctor card) { + super(card); + } + + @Override + public TheNightOfTheDoctor copy() { + return new TheNightOfTheDoctor(this); + } +} + +class TheNightOfTheDoctorEffect extends OneShotEffect { + + private static final Set choices = new LinkedHashSet<>(Arrays.asList( + "First strike", "Vigilance", "Lifelink" + )); + + TheNightOfTheDoctorEffect() { + super(Outcome.PutCardInPlay); + this.staticText = "Return target legendary creature card from your graveyard to the battlefield. Put your choice of a first strike, vigilance, or lifelink counter on it"; + } + + private TheNightOfTheDoctorEffect(final TheNightOfTheDoctorEffect effect) { + super(effect); + } + + @Override + public TheNightOfTheDoctorEffect copy() { + return new TheNightOfTheDoctorEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Card card = game.getCard(getTargetPointer().getFirst(game, source)); + if (card == null || !controller.moveCards(card, Zone.BATTLEFIELD, source, game)) { + return false; + } + Permanent permanent = game.getPermanent(card.getId()); + if (permanent == null) { + return false; + } + Choice choice = new ChoiceImpl(true); + choice.setMessage("Choose first strike, vigilance, or lifelink counter"); + choice.setChoices(choices); + controller.choose(outcome, choice, game); + String chosen = choice.getChoice(); + if (chosen != null) { + permanent.addCounters( + CounterType.findByName(chosen.toLowerCase(Locale.ENGLISH)).createInstance(), + source.getControllerId(), source, game + ); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index cc0497825f0..ebe560652dd 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -988,8 +988,8 @@ public final class DoctorWho extends ExpansionSet { //cards.add(new SetCardInfo("The Moment", 459, Rarity.RARE, mage.cards.t.TheMoment.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("The Moment", 785, Rarity.RARE, mage.cards.t.TheMoment.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("The Moonbase", 591, Rarity.COMMON, mage.cards.t.TheMoonbase.class)); - //cards.add(new SetCardInfo("The Night of the Doctor", 24, Rarity.RARE, mage.cards.t.TheNightOfTheDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The Night of the Doctor", 629, Rarity.RARE, mage.cards.t.TheNightOfTheDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Night of the Doctor", 24, Rarity.RARE, mage.cards.t.TheNightOfTheDoctor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Night of the Doctor", 629, Rarity.RARE, mage.cards.t.TheNightOfTheDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Ninth Doctor", "560z", Rarity.RARE, mage.cards.t.TheNinthDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Ninth Doctor", 1023, Rarity.RARE, mage.cards.t.TheNinthDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Ninth Doctor", 1151, Rarity.RARE, mage.cards.t.TheNinthDoctor.class, NON_FULL_USE_VARIOUS)); From a722d32df203c0ba0d05f649580f7a97fcc57ec0 Mon Sep 17 00:00:00 2001 From: padfoothelix Date: Sat, 15 Mar 2025 01:45:34 +0100 Subject: [PATCH 23/44] [WHO] Implement Ensnared By The Mara (#13418) --- .../src/mage/cards/e/EnsnaredByTheMara.java | 110 ++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 8 +- 2 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/e/EnsnaredByTheMara.java diff --git a/Mage.Sets/src/mage/cards/e/EnsnaredByTheMara.java b/Mage.Sets/src/mage/cards/e/EnsnaredByTheMara.java new file mode 100644 index 00000000000..180cda0c375 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EnsnaredByTheMara.java @@ -0,0 +1,110 @@ +package mage.cards.e; + +import java.util.UUID; +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardImpl; +import mage.cards.CardsImpl; +import mage.cards.CardSetInfo; +import mage.choices.FaceVillainousChoice; +import mage.choices.VillainousChoice; +import mage.constants.*; +import mage.game.Game; +import mage.players.Player; +import mage.MageObject; +import mage.util.CardUtil; + +/** + * + * @author padfoot + */ +public final class EnsnaredByTheMara extends CardImpl { + + public EnsnaredByTheMara(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{R}{R}"); + + + // Each opponent faces a villainous choice -- They exile cards from the top of their library until they exile a nonland card, then you may cast that card without paying its mana cost, or that player exiles the top four cards of their library and Ensnared by the Mara deals damage equal to the total mana value of those exiled cards to that player. + this.getSpellAbility().addEffect(new EnsnaredByTheMaraEffect()); + } + + private EnsnaredByTheMara(final EnsnaredByTheMara card) { + super(card); + } + + @Override + public EnsnaredByTheMara copy() { + return new EnsnaredByTheMara(this); + } +} + +class EnsnaredByTheMaraEffect extends OneShotEffect { + + private static final FaceVillainousChoice choice = new FaceVillainousChoice( + Outcome.PlayForFree, new EnsnaredByTheMaraFirstChoice(), new EnsnaredByTheMaraSecondChoice() + ); + + EnsnaredByTheMaraEffect() { + super(Outcome.Benefit); + staticText = "each opponent " + choice.generateRule(); + } + + private EnsnaredByTheMaraEffect(final EnsnaredByTheMaraEffect effect) { + super(effect); + } + + @Override + public EnsnaredByTheMaraEffect copy() { + return new EnsnaredByTheMaraEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + for (UUID playerId : game.getOpponents(source.getControllerId())) { + Player player = game.getPlayer(playerId); + choice.faceChoice(player, game, source); + } + return true; + } +} + +class EnsnaredByTheMaraFirstChoice extends VillainousChoice { + EnsnaredByTheMaraFirstChoice() { + super("They exile cards from the top of their library until they exile a nonland card, then you may cast that card without paying its mana cost","Exile a card for {controller} to cast for free"); + } + + @Override + public boolean doChoice(Player player, Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + for (Card card : player.getLibrary().getCards(game)) { + player.moveCards(card, Zone.EXILED, source, game); + if (!card.isLand(game)) { + CardUtil.castSpellWithAttributesForFree(controller, source, game, card); + break; + } + } + return true; + } +} + +class EnsnaredByTheMaraSecondChoice extends VillainousChoice { + EnsnaredByTheMaraSecondChoice() { + super("that player exiles the top four cards of their library and {this} deals damage equal to the total mana value of those exiled cards to that player","exile four cards and take damage"); + } + + @Override + public boolean doChoice(Player player, Game game, Ability source) { + int totalManaValue = 0; + Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 4)); + player.moveCards(cards, Zone.EXILED, source, game); + totalManaValue = cards + .getCards(game) + .stream() + .mapToInt(MageObject::getManaValue) + .sum(); + player.damage(totalManaValue, source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index ebe560652dd..6b5cfefc7c5 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -291,10 +291,10 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("Ecstatic Beauty", 688, Rarity.RARE, mage.cards.e.EcstaticBeauty.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Ecstatic Beauty", 83, Rarity.RARE, mage.cards.e.EcstaticBeauty.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Ecstatic Beauty", 974, Rarity.RARE, mage.cards.e.EcstaticBeauty.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Ensnared by the Mara", 384, Rarity.RARE, mage.cards.e.EnsnaredByTheMara.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Ensnared by the Mara", 689, Rarity.RARE, mage.cards.e.EnsnaredByTheMara.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Ensnared by the Mara", 84, Rarity.RARE, mage.cards.e.EnsnaredByTheMara.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Ensnared by the Mara", 975, Rarity.RARE, mage.cards.e.EnsnaredByTheMara.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ensnared by the Mara", 384, Rarity.RARE, mage.cards.e.EnsnaredByTheMara.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ensnared by the Mara", 689, Rarity.RARE, mage.cards.e.EnsnaredByTheMara.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ensnared by the Mara", 84, Rarity.RARE, mage.cards.e.EnsnaredByTheMara.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ensnared by the Mara", 975, Rarity.RARE, mage.cards.e.EnsnaredByTheMara.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Everybody Lives!", 18, Rarity.RARE, mage.cards.e.EverybodyLives.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Everybody Lives!", 338, Rarity.RARE, mage.cards.e.EverybodyLives.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Everybody Lives!", 623, Rarity.RARE, mage.cards.e.EverybodyLives.class, NON_FULL_USE_VARIOUS)); From f76a7bf9db7361dfd8290a161db383093b80ea3c Mon Sep 17 00:00:00 2001 From: im-inuenc Date: Sat, 15 Mar 2025 00:55:43 +0000 Subject: [PATCH 24/44] Implement [FIC] Y'shtola, Night's Blessed (#13381) Co-authored-by: im-inuenc --- .../mage/cards/y/YshtolaNightsBlessed.java | 94 +++++++++++++++++++ .../src/mage/sets/FinalFantasyCommander.java | 4 + 2 files changed, 98 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/y/YshtolaNightsBlessed.java diff --git a/Mage.Sets/src/mage/cards/y/YshtolaNightsBlessed.java b/Mage.Sets/src/mage/cards/y/YshtolaNightsBlessed.java new file mode 100644 index 00000000000..d3e1214ef55 --- /dev/null +++ b/Mage.Sets/src/mage/cards/y/YshtolaNightsBlessed.java @@ -0,0 +1,94 @@ +package mage.cards.y; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.effects.common.DamagePlayersEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.constants.*; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.FilterSpell; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.watchers.common.PlayerLostLifeWatcher; + +/** + * + * @author inuenc + */ +public final class YshtolaNightsBlessed extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("a noncreature spell with mana value 3 or greater"); + + static { + filter.add(Predicates.not(CardType.CREATURE.getPredicate())); + filter.add(new ManaValuePredicate(ComparisonType.MORE_THAN, 2)); + } + + public YshtolaNightsBlessed(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}{U}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.CAT); + this.subtype.add(SubType.WARLOCK); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // At the beginning of each end step, if a player lost 4 or more life this turn, you draw a card. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new BeginningOfEndStepTriggeredAbility( + TargetController.ANY, + new DrawCardSourceControllerEffect(1), + false + ), YshtolaNightsBlessedCondition.instance, "At the beginning of each end step, " + + "if a player lost 4 or more life this turn, you draw a card." + ).addHint(new ConditionHint(YshtolaNightsBlessedCondition.instance, "A player lost 4 or more life this turn"))); + + // Whenever you cast a noncreature spell with mana value 3 or greater, Y'shtola deals 2 damage to each opponent and you gain 2 life. + Ability ability = new SpellCastControllerTriggeredAbility( + new DamagePlayersEffect( + 2, TargetController.OPPONENT, "Y'shtola"), + filter, false + ); + ability.addEffect(new GainLifeEffect(2).setText("and you gain 2 life")); + this.addAbility(ability); + } + + private YshtolaNightsBlessed(final YshtolaNightsBlessed card) { + super(card); + } + + @Override + public YshtolaNightsBlessed copy() { + return new YshtolaNightsBlessed(this); + } +} + +enum YshtolaNightsBlessedCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + PlayerLostLifeWatcher watcher = game.getState().getWatcher(PlayerLostLifeWatcher.class); + if (watcher == null) { + return false; + } + return game + .getState() + .getPlayersInRange(source.getControllerId(), game) + .stream() + .anyMatch(uuid -> watcher.getLifeLost(uuid) > 3); + } +} diff --git a/Mage.Sets/src/mage/sets/FinalFantasyCommander.java b/Mage.Sets/src/mage/sets/FinalFantasyCommander.java index 2ab48df6768..4ff84c01c39 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasyCommander.java +++ b/Mage.Sets/src/mage/sets/FinalFantasyCommander.java @@ -20,5 +20,9 @@ public final class FinalFantasyCommander extends ExpansionSet { this.hasBasicLands = false; cards.add(new SetCardInfo("Terra, Herald of Hope", 4, Rarity.MYTHIC, mage.cards.t.TerraHeraldOfHope.class)); + cards.add(new SetCardInfo("Y'shtola, Night's Blessed", 7, Rarity.MYTHIC, mage.cards.y.YshtolaNightsBlessed.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Y'shtola, Night's Blessed", 191, Rarity.MYTHIC, mage.cards.y.YshtolaNightsBlessed.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Y'shtola, Night's Blessed", 215, Rarity.MYTHIC, mage.cards.y.YshtolaNightsBlessed.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Y'shtola, Night's Blessed", 226, Rarity.MYTHIC, mage.cards.y.YshtolaNightsBlessed.class, NON_FULL_USE_VARIOUS)); } } From 4629456a920cd3d2e50bca546d1559c7e3b61186 Mon Sep 17 00:00:00 2001 From: im-inuenc Date: Sat, 15 Mar 2025 00:55:57 +0000 Subject: [PATCH 25/44] [DSC] Implement Ursine Monstrosity (#13388) --------- Co-authored-by: im-inuenc --- .../src/mage/cards/u/UrsineMonstrosity.java | 61 +++++++++++++++++++ .../sets/DuskmournHouseOfHorrorCommander.java | 2 + 2 files changed, 63 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/u/UrsineMonstrosity.java diff --git a/Mage.Sets/src/mage/cards/u/UrsineMonstrosity.java b/Mage.Sets/src/mage/cards/u/UrsineMonstrosity.java new file mode 100644 index 00000000000..423dad34aaf --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UrsineMonstrosity.java @@ -0,0 +1,61 @@ +package mage.cards.u; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.dynamicvalue.common.CardTypesInGraveyardCount; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.combat.AttackIfAbleTargetRandomOpponentSourceEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; + +/** + * + * @author inuenc + */ +public final class UrsineMonstrosity extends CardImpl { + + public UrsineMonstrosity(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.BEAR); + this.subtype.add(SubType.MUTANT); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // At the beginning of combat on your turn, mill a card and choose an opponent at random. Ursine Monstrosity attacks that player this combat if able. Until end of turn, Ursine Monstrosity gains indestructible and gets +1/+1 for each card type among cards in your graveyard. + BeginningOfCombatTriggeredAbility ability = new BeginningOfCombatTriggeredAbility( + new MillCardsControllerEffect(1) + ); + ability.addEffect(new AttackIfAbleTargetRandomOpponentSourceEffect() + .concatBy("and") + ); + ability.addEffect(new GainAbilitySourceEffect(IndestructibleAbility.getInstance(), Duration.EndOfTurn) + .setText("Until end of turn, {this} gains indestructible") + ); + ability.addEffect(new BoostSourceEffect(CardTypesInGraveyardCount.YOU, CardTypesInGraveyardCount.YOU, Duration.EndOfTurn) + .setText("gets +1/+1 for each card type among cards in your graveyard.") + .concatBy("and") + ); + this.addAbility(ability.addHint(CardTypesInGraveyardCount.YOU.getHint())); + } + + private UrsineMonstrosity(final UrsineMonstrosity card) { + super(card); + } + + @Override + public UrsineMonstrosity copy() { + return new UrsineMonstrosity(this); + } +} diff --git a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java index 4b74280d21f..10ec645b634 100644 --- a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java +++ b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java @@ -289,6 +289,8 @@ public final class DuskmournHouseOfHorrorCommander extends ExpansionSet { cards.add(new SetCardInfo("Trygon Predator", 238, Rarity.UNCOMMON, mage.cards.t.TrygonPredator.class)); cards.add(new SetCardInfo("Twilight Mire", 320, Rarity.RARE, mage.cards.t.TwilightMire.class)); cards.add(new SetCardInfo("Underground River", 321, Rarity.RARE, mage.cards.u.UndergroundRiver.class)); + cards.add(new SetCardInfo("Ursine Monstrosity", 36, Rarity.RARE, mage.cards.u.UrsineMonstrosity.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Ursine Monstrosity", 63, Rarity.RARE, mage.cards.u.UrsineMonstrosity.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Utter End", 91, Rarity.RARE, mage.cards.u.UtterEnd.class)); cards.add(new SetCardInfo("Valgavoth, Harrower of Souls", 6, Rarity.MYTHIC, mage.cards.v.ValgavothHarrowerOfSouls.class)); cards.add(new SetCardInfo("Vault of Whispers", 322, Rarity.COMMON, mage.cards.v.VaultOfWhispers.class)); From 4376ae63a3af184fe38c637bf4c38b817b29abb9 Mon Sep 17 00:00:00 2001 From: padfoothelix Date: Sat, 15 Mar 2025 01:56:09 +0100 Subject: [PATCH 26/44] Implement [DSC] Suspended Sentence (#13434) --- .../src/mage/cards/s/SuspendedSentence.java | 43 +++++++++++++++++++ .../sets/DuskmournHouseOfHorrorCommander.java | 1 + 2 files changed, 44 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SuspendedSentence.java diff --git a/Mage.Sets/src/mage/cards/s/SuspendedSentence.java b/Mage.Sets/src/mage/cards/s/SuspendedSentence.java new file mode 100644 index 00000000000..e8d66bb1ebb --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SuspendedSentence.java @@ -0,0 +1,43 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.effects.common.ExileSpellWithTimeCountersEffect; +import mage.abilities.effects.common.LoseLifeTargetControllerEffect; +import mage.abilities.keyword.SuspendAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.common.TargetOpponentsCreaturePermanent; + +/** + * + * @author padfoothelix + */ +public final class SuspendedSentence extends CardImpl { + + public SuspendedSentence(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{B}"); + + + // Destroy target creature an opponent controls. That player loses 3 life. Exile Suspended Sentence with three time counters on it. + this.getSpellAbility().addEffect(new DestroyTargetEffect()); + this.getSpellAbility().addEffect(new LoseLifeTargetControllerEffect(3).setText("That player loses 3 life")); + this.getSpellAbility().addTarget(new TargetOpponentsCreaturePermanent()); + this.getSpellAbility().addEffect(new ExileSpellWithTimeCountersEffect(3)); + + // Suspend 3--{1}{B} + this.addAbility(new SuspendAbility(3, new ManaCostsImpl<>("{1}{B}"), this)); + + } + + private SuspendedSentence(final SuspendedSentence card) { + super(card); + } + + @Override + public SuspendedSentence copy() { + return new SuspendedSentence(this); + } +} diff --git a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java index 10ec645b634..427c05d2e0c 100644 --- a/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java +++ b/Mage.Sets/src/mage/sets/DuskmournHouseOfHorrorCommander.java @@ -246,6 +246,7 @@ public final class DuskmournHouseOfHorrorCommander extends ExpansionSet { cards.add(new SetCardInfo("Stitcher's Supplier", 157, Rarity.UNCOMMON, mage.cards.s.StitchersSupplier.class)); cards.add(new SetCardInfo("Stormfist Crusader", 234, Rarity.RARE, mage.cards.s.StormfistCrusader.class)); cards.add(new SetCardInfo("Sulfurous Springs", 301, Rarity.RARE, mage.cards.s.SulfurousSprings.class)); + cards.add(new SetCardInfo("Suspended Sentence", 25, Rarity.RARE, mage.cards.s.SuspendedSentence.class)); cards.add(new SetCardInfo("Suspicious Bookcase", 95, Rarity.UNCOMMON, mage.cards.s.SuspiciousBookcase.class)); cards.add(new SetCardInfo("Swords to Plowshares", 106, Rarity.UNCOMMON, mage.cards.s.SwordsToPlowshares.class)); cards.add(new SetCardInfo("Syr Konrad, the Grim", 158, Rarity.UNCOMMON, mage.cards.s.SyrKonradTheGrim.class)); From 69f6c343f327259e7c0567512708e1e6de884661 Mon Sep 17 00:00:00 2001 From: cjohnson714 Date: Fri, 14 Mar 2025 20:57:32 -0400 Subject: [PATCH 27/44] Fix Terra, Herald of Hope combat trigger (#13371) --- Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java b/Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java index 4ee0be2a5ad..47529f6f42e 100644 --- a/Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java +++ b/Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java @@ -51,7 +51,7 @@ public final class TerraHeraldOfHope extends CardImpl { ReflexiveTriggeredAbility reflexiveAbility = new ReflexiveTriggeredAbility( new ReturnFromGraveyardToBattlefieldTargetEffect(true), false ); - ability.addTarget(new TargetCardInYourGraveyard(filter)); + reflexiveAbility.addTarget(new TargetCardInYourGraveyard(filter)); this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( new DoWhenCostPaid(reflexiveAbility, new GenericManaCost(2), "Pay {2}?") )); From d90e50ba9831f4ac19cafc9d210bd08b5c6dc5a8 Mon Sep 17 00:00:00 2001 From: grimreap124 <19590931+grimreap124@users.noreply.github.com> Date: Fri, 14 Mar 2025 21:30:16 -0400 Subject: [PATCH 28/44] Implement [DRC] Stridehangar Automaton (#13410) --- .../mage/cards/s/StridehangarAutomaton.java | 104 ++++++++++++++++++ .../src/mage/sets/AetherdriftCommander.java | 1 + 2 files changed, 105 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StridehangarAutomaton.java diff --git a/Mage.Sets/src/mage/cards/s/StridehangarAutomaton.java b/Mage.Sets/src/mage/cards/s/StridehangarAutomaton.java new file mode 100644 index 00000000000..eda853ef01d --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StridehangarAutomaton.java @@ -0,0 +1,104 @@ +package mage.cards.s; + +import java.util.Map; +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.constants.*; +import mage.filter.common.FilterCreaturePermanent; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.game.Game; +import mage.game.permanent.token.Token; +import mage.game.events.CreateTokenEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.token.ThopterColorlessToken; + + +/** + * @author grimreap124 + */ +public final class StridehangarAutomaton extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(SubType.THOPTER, "Thopters"); + + public StridehangarAutomaton(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{3}"); + + this.subtype.add(SubType.CONSTRUCT); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // Thopters you control get +1/+1. + this.addAbility(new SimpleStaticAbility(new BoostControlledEffect( + 1, 1, Duration.WhileOnBattlefield, filter + ))); + // If one or more artifact tokens would be created under your control, those tokens plus an additional 1/1 colorless Thopter artifact creature token with flying are created instead. + this.addAbility(new SimpleStaticAbility(new StridehangarAutomatonReplacementEffect())); + } + + private StridehangarAutomaton(final StridehangarAutomaton card) { + super(card); + } + + @Override + public StridehangarAutomaton copy() { + return new StridehangarAutomaton(this); + } +} + +class StridehangarAutomatonReplacementEffect extends ReplacementEffectImpl { + + StridehangarAutomatonReplacementEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + this.staticText = "If one or more artifact tokens would be created under your control, those tokens plus an additional 1/1 colorless Thopter artifact creature token with flying are created instead"; + } + + private StridehangarAutomatonReplacementEffect(final StridehangarAutomatonReplacementEffect effect) { + super(effect); + } + + @Override + public StridehangarAutomatonReplacementEffect copy() { + return new StridehangarAutomatonReplacementEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.CREATE_TOKEN; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + if (source.isControlledBy(event.getPlayerId())) { + for (Map.Entry entry : ((CreateTokenEvent) event).getTokens().entrySet()) { + if (entry.getValue() > 0 && entry.getKey().isArtifact(game)) { + return true; + } + } + } + return false; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + if (event instanceof CreateTokenEvent) { + CreateTokenEvent tokenEvent = (CreateTokenEvent) event; + ThopterColorlessToken thopterToken = null; + Map tokens = tokenEvent.getTokens(); + for (Map.Entry entry : tokens.entrySet()) { + if (entry.getKey() instanceof ThopterColorlessToken) { + thopterToken = (ThopterColorlessToken) entry.getKey(); + } + } + if (thopterToken == null) { + thopterToken = new ThopterColorlessToken(); + } + tokens.put(thopterToken, tokens.getOrDefault(thopterToken, 0) + 1); + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/AetherdriftCommander.java b/Mage.Sets/src/mage/sets/AetherdriftCommander.java index 10328d49a35..c3f44d37971 100644 --- a/Mage.Sets/src/mage/sets/AetherdriftCommander.java +++ b/Mage.Sets/src/mage/sets/AetherdriftCommander.java @@ -147,6 +147,7 @@ public final class AetherdriftCommander extends ExpansionSet { cards.add(new SetCardInfo("Solemn Simulacrum", 138, Rarity.RARE, mage.cards.s.SolemnSimulacrum.class)); cards.add(new SetCardInfo("Soul-Guide Lantern", 139, Rarity.UNCOMMON, mage.cards.s.SoulGuideLantern.class)); cards.add(new SetCardInfo("Spire of Industry", 172, Rarity.RARE, mage.cards.s.SpireOfIndustry.class)); + cards.add(new SetCardInfo("Stridehangar Automaton", 19, Rarity.RARE, mage.cards.s.StridehangarAutomaton.class)); cards.add(new SetCardInfo("Sulfur Falls", 173, Rarity.RARE, mage.cards.s.SulfurFalls.class)); cards.add(new SetCardInfo("Sunken Hollow", 174, Rarity.RARE, mage.cards.s.SunkenHollow.class)); cards.add(new SetCardInfo("Swords to Plowshares", 37, Rarity.UNCOMMON, mage.cards.s.SwordsToPlowshares.class)); From 388cde0cdd7ddb7242a6f055f191da2d44ad25f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Krist=C3=B3f?= <20043803+balazskristof@users.noreply.github.com> Date: Sat, 15 Mar 2025 02:30:27 +0100 Subject: [PATCH 29/44] [FIC] Implement Cloud, Ex-SOLDIER (#13441) --- .../src/mage/cards/c/CloudExSOLDIER.java | 123 ++++++++++++++++++ .../src/mage/sets/FinalFantasyCommander.java | 4 + 2 files changed, 127 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CloudExSOLDIER.java diff --git a/Mage.Sets/src/mage/cards/c/CloudExSOLDIER.java b/Mage.Sets/src/mage/cards/c/CloudExSOLDIER.java new file mode 100644 index 00000000000..722d1801066 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CloudExSOLDIER.java @@ -0,0 +1,123 @@ +package mage.cards.c; + +import java.util.UUID; +import mage.MageInt; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledArtifactPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.common.FilterEquipmentPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.filter.predicate.permanent.AttackingPredicate; +import mage.filter.predicate.permanent.EnchantedPredicate; +import mage.filter.predicate.permanent.EquippedPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.InsectToken; +import mage.game.permanent.token.TreasureToken; +import mage.target.TargetPermanent; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.condition.common.SourceMatchesFilterCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Outcome; + +/** + * + * @author balazskristof + */ +public final class CloudExSOLDIER extends CardImpl { + + private static final FilterControlledArtifactPermanent filter = new FilterControlledArtifactPermanent("Equipment you control"); + private static final FilterControlledCreaturePermanent filter2 = new FilterControlledCreaturePermanent("equipped attacking creature you control"); + private static final FilterCreaturePermanent filter3 = new FilterCreaturePermanent(); + + static { + filter.add(SubType.EQUIPMENT.getPredicate()); + filter2.add(Predicates.and( + AttackingPredicate.instance, + EquippedPredicate.instance + )); + filter3.add(new PowerPredicate(ComparisonType.MORE_THAN, 6)); + } + + static { + filter.add(TargetController.YOU.getControllerPredicate()); + } + + public CloudExSOLDIER(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}{G}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.SOLDIER); + this.subtype.add(SubType.MERCENARY); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // When Cloud enters, attach up to one target Equipment you control to it. + Ability ability = new EntersBattlefieldTriggeredAbility(new CloudExSOLDIEREntersEffect()); + ability.addTarget(new TargetPermanent(0, 1, filter)); + this.addAbility(ability); + + // Whenever Cloud attacks, draw a card for each equipped attacking creature you control. Then if Cloud has power 7 or greater, create two Treasure tokens. + Ability ability2 = new AttacksTriggeredAbility(new DrawCardSourceControllerEffect(new PermanentsOnBattlefieldCount(filter2))); + ability2.addEffect(new ConditionalOneShotEffect( + new CreateTokenEffect(new TreasureToken(), 2), + new SourceMatchesFilterCondition(filter3), + "Then if {this} has power 7 or greater, create two Treasure tokens." + )); + this.addAbility(ability2); + } + + private CloudExSOLDIER(final CloudExSOLDIER card) { + super(card); + } + + @Override + public CloudExSOLDIER copy() { + return new CloudExSOLDIER(this); + } +} + + +class CloudExSOLDIEREntersEffect extends OneShotEffect { + + CloudExSOLDIEREntersEffect() { + super(Outcome.Benefit); + staticText = "attach up to one target Equipment you control to {this}"; + } + + private CloudExSOLDIEREntersEffect(final CloudExSOLDIEREntersEffect effect) { + super(effect); + } + + @Override + public CloudExSOLDIEREntersEffect copy() { + return new CloudExSOLDIEREntersEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent equipment = game.getPermanent(this.getTargetPointer().getFirst(game, source)); + Permanent creature = source.getSourcePermanentIfItStillExists(game); + return equipment != null && creature != null && creature.addAttachment(equipment.getId(), source, game); + } +} diff --git a/Mage.Sets/src/mage/sets/FinalFantasyCommander.java b/Mage.Sets/src/mage/sets/FinalFantasyCommander.java index 4ff84c01c39..757f5c0cc62 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasyCommander.java +++ b/Mage.Sets/src/mage/sets/FinalFantasyCommander.java @@ -19,6 +19,10 @@ public final class FinalFantasyCommander extends ExpansionSet { super("Final Fantasy Commander", "FIC", ExpansionSet.buildDate(2025, 6, 13), SetType.SUPPLEMENTAL); this.hasBasicLands = false; + cards.add(new SetCardInfo("Cloud, Ex-SOLDIER", 2, Rarity.MYTHIC, mage.cards.c.CloudExSOLDIER.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Cloud, Ex-SOLDIER", 168, Rarity.MYTHIC, mage.cards.c.CloudExSOLDIER.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Cloud, Ex-SOLDIER", 202, Rarity.MYTHIC, mage.cards.c.CloudExSOLDIER.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Cloud, Ex-SOLDIER", 221, Rarity.MYTHIC, mage.cards.c.CloudExSOLDIER.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Terra, Herald of Hope", 4, Rarity.MYTHIC, mage.cards.t.TerraHeraldOfHope.class)); cards.add(new SetCardInfo("Y'shtola, Night's Blessed", 7, Rarity.MYTHIC, mage.cards.y.YshtolaNightsBlessed.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Y'shtola, Night's Blessed", 191, Rarity.MYTHIC, mage.cards.y.YshtolaNightsBlessed.class, NON_FULL_USE_VARIOUS)); From f00e86c47a4c978c032678361aefc3d6fcffa2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bal=C3=A1zs=20Krist=C3=B3f?= <20043803+balazskristof@users.noreply.github.com> Date: Sat, 15 Mar 2025 02:30:36 +0100 Subject: [PATCH 30/44] [TDM] Implement Rally the Monastery (#13447) --- .../src/mage/cards/r/RallyTheMonastery.java | 103 ++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + Utils/mtg-cards-data.txt | 1 + 3 files changed, 105 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/r/RallyTheMonastery.java diff --git a/Mage.Sets/src/mage/cards/r/RallyTheMonastery.java b/Mage.Sets/src/mage/cards/r/RallyTheMonastery.java new file mode 100644 index 00000000000..335b01cee14 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RallyTheMonastery.java @@ -0,0 +1,103 @@ +package mage.cards.r; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DestroyTargetEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.game.Game; +import mage.game.permanent.token.BirdToken; +import mage.game.permanent.token.MonasteryMentorToken; +import mage.game.stack.Spell; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.watchers.common.SpellsCastWatcher; + +/** + * @author balazskristof + */ +public final class RallyTheMonastery extends CardImpl { + + private static final Hint hint = new ConditionHint( + RallyTheMonasteryCondition.instance, "You've cast a spell this turn" + ); + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("creature with power 4 or greater"); + + static { + filter.add(new PowerPredicate(ComparisonType.MORE_THAN, 3)); + } + + public RallyTheMonastery(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{3}{W}"); + + // This spell costs {2} less to cast if you've cast another spell this turn. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, + new SpellCostReductionSourceEffect(2, RallyTheMonasteryCondition.instance).setCanWorksOnStackOnly(true) + ).setRuleAtTheTop(true).addHint(hint)); + + // Choose one — + this.getSpellAbility().getModes().setMinModes(1); + this.getSpellAbility().getModes().setMaxModes(1); + // • Create two 1/1 white Monk creature tokens with prowess. + this.getSpellAbility().addEffect(new CreateTokenEffect(new MonasteryMentorToken(), 2)); + // • Up to two target creatures you control each get +2/+2 until end of turn. + Mode mode = new Mode(new BoostTargetEffect(2, 2)); + mode.addTarget(new TargetControlledCreaturePermanent(0, 2, StaticFilters.FILTER_CONTROLLED_CREATURES, false)); + this.getSpellAbility().getModes().addMode(mode); + // • Destroy target creature with power 4 or greater. + Mode mode2 = new Mode(new DestroyTargetEffect()); + mode2.addTarget(new TargetPermanent(filter)); + this.getSpellAbility().getModes().addMode(mode2); + } + + private RallyTheMonastery(final RallyTheMonastery card) { + super(card); + } + + @Override + public RallyTheMonastery copy() { + return new RallyTheMonastery(this); + } +} + +enum RallyTheMonasteryCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (watcher == null) { + return false; + } + List spells = watcher.getSpellsCastThisTurn(source.getControllerId()); + return spells != null && spells + .stream() + .filter(Objects::nonNull) + .anyMatch(spell -> !spell.getSourceId().equals(source.getSourceId())); + } + + @Override + public String toString() { + return "you've cast another spell this turn"; + } +} + diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 4346ffbc666..853dbf7dc8a 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -24,6 +24,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Inevitable Defeat", 194, Rarity.RARE, mage.cards.i.InevitableDefeat.class)); cards.add(new SetCardInfo("Mox Jasper", 246, Rarity.MYTHIC, mage.cards.m.MoxJasper.class)); cards.add(new SetCardInfo("Narset, Jeskai Waymaster", 209, Rarity.RARE, mage.cards.n.NarsetJeskaiWaymaster.class)); + cards.add(new SetCardInfo("Rally the Monastery", 19, Rarity.UNCOMMON, mage.cards.r.RallyTheMonastery.class)); cards.add(new SetCardInfo("Shiko, Paragon of the Way", 223, Rarity.MYTHIC, mage.cards.s.ShikoParagonOfTheWay.class)); cards.add(new SetCardInfo("Skirmish Rhino", 224, Rarity.UNCOMMON, mage.cards.s.SkirmishRhino.class)); cards.add(new SetCardInfo("Smile at Death", 24, Rarity.MYTHIC, mage.cards.s.SmileAtDeath.class)); diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index d585e63d623..705fd64ef1a 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -57197,6 +57197,7 @@ Chaos, the Endless|Final Fantasy|221|U||Legendary Creature - Demon|5|5|Flying$Wh Sin, Spira's Punishment|Final Fantasy|242|R|{4}{B}{G}{U}|Legendary Creature - Leviathan Avatar|7|7|Flying$Whenever Sin enters or attacks, exile a permanent card from your graveyard at random, then create a tapped token that's a copy of that card. If the exiled card is a land card, repeat this process.| Cloud, Planet's Champion|Final Fantasy|552|M|{3}{R}{W}|Legendary Creature - Human Soldier Mercenary|4|4|During your turn, as long as Cloud is equipped, it has double strike and indestructible.$Equip abilities you activate that target Cloud cost {2} less to activate.| Sephiroth, Planet's Heir|Final Fantasy|553|M|{4}{U}{B}|Legendary Creature - Human Avatar Soldier|4|4|Vigilance$When Sephiroth enters, creatures your opponents control get -2/-2 until end of turn.$Whenever a creature an opponent controls dies, put a +1/+1 counter on Sephiroth.| +Rally the Monastery|Tarkir: Dragonstorm|19|U|{3}{W}|Instant|||This spell costs {2} less to cast if you've cast another spell this turn.$Choose one —$• Create two 1/1 white Monk creature tokens with prowess.$• Up to two target creatures you control each get +2/+2 until end of turn.$• Destroy target creature with power 4 or greater.| Smile at Death|Tarkir: Dragonstorm|24|M|{3}{W}{W}|Enchantment|||At the beginning of your upkeep, return up to two target creature cards with power 2 or less from your graveyard to the battlefield. Put a +1/+1 counter on each of those creatures.| Sarkhan, Dragon Ascendant|Tarkir: Dragonstorm|118|R|{1}{R}|Legendary Creature - Human Druid|2|2|When Sarkhan enters, you may behold a Dragon. If you do, create a Treasure token.$Whenever a Dragon you control enters, put a +1/+1 counter on Sarkhan. Until end of turn, Sarkhan becomes a Dragon in addition to its other types and gains flying.| Stormscale Scion|Tarkir: Dragonstorm|123|M|{4}{R}{R}|Creature - Dragon|4|4|Flying$Other Dragons you control get +1/+1.$Storm| From f252525cb825e3bd5b53897dd214d3ab29f1271e Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 15 Mar 2025 16:33:37 -0400 Subject: [PATCH 31/44] fix Minion of the Wastes, add test --- .../src/mage/cards/m/MinionOfTheWastes.java | 30 +++++++++++----- .../single/tmp/MinionOfTheWastesTest.java | 35 +++++++++++++++++++ 2 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MinionOfTheWastesTest.java diff --git a/Mage.Sets/src/mage/cards/m/MinionOfTheWastes.java b/Mage.Sets/src/mage/cards/m/MinionOfTheWastes.java index 159d5b78b4d..db1bb56c139 100644 --- a/Mage.Sets/src/mage/cards/m/MinionOfTheWastes.java +++ b/Mage.Sets/src/mage/cards/m/MinionOfTheWastes.java @@ -6,7 +6,7 @@ import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.common.PayLifeCost; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.SetBasePowerToughnessSourceEffect; import mage.abilities.keyword.TrampleAbility; @@ -15,6 +15,8 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.game.Game; +import mage.game.events.EntersTheBattlefieldEvent; +import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; @@ -52,10 +54,10 @@ public final class MinionOfTheWastes extends CardImpl { } } -class MinionOfTheWastesEffect extends OneShotEffect { +class MinionOfTheWastesEffect extends ReplacementEffectImpl { MinionOfTheWastesEffect() { - super(Outcome.LoseLife); + super(Duration.EndOfGame, Outcome.LoseLife); staticText = "pay any amount of life"; } @@ -69,10 +71,20 @@ class MinionOfTheWastesEffect extends OneShotEffect { } @Override - public boolean apply(Game game, Ability source) { + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return event.getTargetId().equals(source.getSourceId()); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Permanent creature = ((EntersTheBattlefieldEvent) event).getTarget(); Player controller = game.getPlayer(source.getControllerId()); - Permanent permanent = game.getPermanentEntering(source.getSourceId()); - if (controller == null || permanent == null) { + if (creature == null || controller == null) { return false; } int payAmount = controller.getAmount(0, controller.getLife(), "Pay any amount of life", game); @@ -84,9 +96,9 @@ class MinionOfTheWastesEffect extends OneShotEffect { game.informPlayers((sourceCard != null ? sourceCard.getLogName() : "") + ": " + controller.getLogName() + " pays " + payAmount + " life"); game.addEffect(new SetBasePowerToughnessSourceEffect( - payAmount, payAmount, Duration.Custom + payAmount, payAmount, Duration.WhileOnBattlefield ), source); - permanent.addInfo("life paid", CardUtil.addToolTipMarkTags("Life paid: " + payAmount), game); - return true; + creature.addInfo("life paid", CardUtil.addToolTipMarkTags("Life paid: " + payAmount), game); + return false; } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MinionOfTheWastesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MinionOfTheWastesTest.java new file mode 100644 index 00000000000..2d7e79a20f8 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MinionOfTheWastesTest.java @@ -0,0 +1,35 @@ +package org.mage.test.cards.single.tmp; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class MinionOfTheWastesTest extends CardTestPlayerBase { + + private static final String minion = "Minion of the Wastes"; + /* Trample + * As this creature enters, pay any amount of life. + * Minion of the Wastes’s power and toughness are each equal to the life paid as it entered. + */ + + @Test + public void testSimple() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6); + addCard(Zone.HAND, playerA, minion); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, minion); + setChoice(playerA, "X=3"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 17); + assertPowerToughness(playerA, minion, 3, 3); + } + +} From 6a6a8364c3f0bdc05613110464f30d62359906d0 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 15 Mar 2025 16:37:47 -0400 Subject: [PATCH 32/44] fix Wood Elemental --- Mage.Sets/src/mage/cards/w/WoodElemental.java | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/Mage.Sets/src/mage/cards/w/WoodElemental.java b/Mage.Sets/src/mage/cards/w/WoodElemental.java index d84810fd48a..4f2b29a430c 100644 --- a/Mage.Sets/src/mage/cards/w/WoodElemental.java +++ b/Mage.Sets/src/mage/cards/w/WoodElemental.java @@ -1,29 +1,26 @@ - package mage.cards.w; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.AsEntersBattlefieldAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.SetBasePowerToughnessSourceEffect; -import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.CardType; -import mage.constants.SubType; -import mage.constants.Duration; -import mage.constants.Outcome; -import mage.constants.Zone; +import mage.constants.*; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.permanent.TappedPredicate; import mage.game.Game; +import mage.game.events.EntersTheBattlefieldEvent; +import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.Target; -import mage.target.common.TargetControlledPermanent; +import mage.target.common.TargetSacrifice; + +import java.util.UUID; /** * @@ -54,7 +51,7 @@ public final class WoodElemental extends CardImpl { } } -class WoodElementalEffect extends OneShotEffect { +class WoodElementalEffect extends ReplacementEffectImpl { private static final FilterControlledPermanent filter = new FilterControlledPermanent("untapped Forests you control"); @@ -64,7 +61,7 @@ class WoodElementalEffect extends OneShotEffect { } public WoodElementalEffect() { - super(Outcome.Sacrifice); + super(Duration.EndOfGame, Outcome.Sacrifice); staticText = "sacrifice any number of untapped Forests"; } @@ -78,27 +75,38 @@ class WoodElementalEffect extends OneShotEffect { } @Override - public boolean apply(Game game, Ability source) { + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return event.getTargetId().equals(source.getSourceId()); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Permanent creature = ((EntersTheBattlefieldEvent) event).getTarget(); Player controller = game.getPlayer(source.getControllerId()); - Card sourceCard = game.getCard(source.getSourceId()); - if (controller != null && sourceCard != null) { - Target target = new TargetControlledPermanent(0, Integer.MAX_VALUE, filter, true); - if (target.canChoose(source.getControllerId(), source, game) - && controller.chooseTarget(Outcome.Detriment, target, source, game)) { - if (!target.getTargets().isEmpty()) { - int sacrificedForests = target.getTargets().size(); - game.informPlayers(controller.getLogName() + " sacrifices " + sacrificedForests + " untapped Forests for " + sourceCard.getLogName()); - for (UUID targetId : target.getTargets()) { - Permanent targetPermanent = game.getPermanent(targetId); - if (targetPermanent != null) { - targetPermanent.sacrifice(source, game); - } - } - game.addEffect(new SetBasePowerToughnessSourceEffect(sacrificedForests, sacrificedForests, Duration.Custom), source); - return true; - } + if (creature == null || controller == null) { + return false; + } + Target target = new TargetSacrifice(0, Integer.MAX_VALUE, filter); + if (!target.canChoose(source.getControllerId(), source, game)) { + return false; + } + controller.choose(Outcome.Detriment, target, source, game); + if (target.getTargets().isEmpty()) { + return false; + } + int value = 0; + for (UUID targetId : target.getTargets()) { + Permanent targetCreature = game.getPermanent(targetId); + if (targetCreature != null && targetCreature.sacrifice(source, game)) { + value++; } } + game.addEffect(new SetBasePowerToughnessSourceEffect(value, value, Duration.WhileOnBattlefield), source); return false; } } From e373c82b0a78347f8c2baa66cfcdbd9b2e394ad9 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 15 Mar 2025 16:41:27 -0400 Subject: [PATCH 33/44] fix Nameless Race --- Mage.Sets/src/mage/cards/n/NamelessRace.java | 30 ++++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/Mage.Sets/src/mage/cards/n/NamelessRace.java b/Mage.Sets/src/mage/cards/n/NamelessRace.java index 94ea4af158e..082a06b0516 100644 --- a/Mage.Sets/src/mage/cards/n/NamelessRace.java +++ b/Mage.Sets/src/mage/cards/n/NamelessRace.java @@ -9,7 +9,7 @@ import mage.abilities.costs.Cost; import mage.abilities.costs.common.PayLifeCost; import mage.abilities.dynamicvalue.common.CardsInAllGraveyardsCount; import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.continuous.SetBasePowerToughnessSourceEffect; import mage.abilities.keyword.TrampleAbility; @@ -22,6 +22,8 @@ import mage.filter.FilterPermanent; import mage.filter.predicate.mageobject.ColorPredicate; import mage.filter.predicate.permanent.TokenPredicate; import mage.game.Game; +import mage.game.events.EntersTheBattlefieldEvent; +import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; @@ -58,7 +60,7 @@ public final class NamelessRace extends CardImpl { } } -class NamelessRaceEffect extends OneShotEffect { +class NamelessRaceEffect extends ReplacementEffectImpl { private static final FilterPermanent filter = new FilterPermanent("white nontoken permanents your opponents control"); @@ -74,7 +76,7 @@ class NamelessRaceEffect extends OneShotEffect { } NamelessRaceEffect() { - super(Outcome.LoseLife); + super(Duration.EndOfGame, Outcome.LoseLife); staticText = "pay any amount of life. The amount you pay can't be more than " + "the total number of white nontoken permanents your opponents control " + "plus the total number of white cards in their graveyards"; @@ -90,10 +92,20 @@ class NamelessRaceEffect extends OneShotEffect { } @Override - public boolean apply(Game game, Ability source) { + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return event.getTargetId().equals(source.getSourceId()); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Permanent creature = ((EntersTheBattlefieldEvent) event).getTarget(); Player controller = game.getPlayer(source.getControllerId()); - Permanent permanent = game.getPermanentEntering(source.getSourceId()); - if (controller == null || permanent == null) { + if (creature == null || controller == null) { return false; } int permanentsInPlay = new PermanentsOnBattlefieldCount(filter).calculate(game, source, null); @@ -108,9 +120,9 @@ class NamelessRaceEffect extends OneShotEffect { game.informPlayers((sourceCard != null ? sourceCard.getLogName() : "") + ": " + controller.getLogName() + " pays " + payAmount + " life"); game.addEffect(new SetBasePowerToughnessSourceEffect( - payAmount, payAmount, Duration.Custom + payAmount, payAmount, Duration.WhileOnBattlefield ), source); - permanent.addInfo("life paid", CardUtil.addToolTipMarkTags("Life paid: " + payAmount), game); - return true; + creature.addInfo("life paid", CardUtil.addToolTipMarkTags("Life paid: " + payAmount), game); + return false; } } From bf8c8c4e99b1488f5b9c68dc1ed4aa06a09aadd6 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:36:46 -0400 Subject: [PATCH 34/44] fix #13425 (ninjutsu ability), add test --- .../abilities/keywords/NinjutsuTest.java | 55 +++++++++++++++++++ .../abilities/keyword/NinjutsuAbility.java | 18 ++++-- 2 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/NinjutsuTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/NinjutsuTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/NinjutsuTest.java new file mode 100644 index 00000000000..3b844e5a1bb --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/NinjutsuTest.java @@ -0,0 +1,55 @@ +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; + +/** + 702.49. Ninjutsu + 702.49a. Ninjutsu is an activated ability that functions only while the card with ninjutsu is in a player's hand. + "Ninjutsu [cost]" means "[Cost], Reveal this card from your hand, Return an unblocked attacking creature you control + to its owner's hand: Put this card onto the battlefield from your hand tapped and attacking." + 702.49b. The card with ninjutsu remains revealed from the time the ability is announced until the ability leaves the stack. + 702.49c. A ninjutsu ability may be activated only while a creature on the battlefield is unblocked (see rule 509.1h). + The creature with ninjutsu is put onto the battlefield unblocked. It will be attacking the same player, planeswalker, + or battle as the creature that was returned to its owner's hand. + 702.49d. Commander ninjutsu is a variant of the ninjutsu ability that also functions while the card with commander + ninjutsu is in the command zone. "Commander ninjutsu [cost]" means "[Cost], Reveal this card from your hand or + from the command zone, Return an unblocked attacking creature you control to its owner's hand: Put this card onto + the battlefield tapped and attacking." + */ +public class NinjutsuTest extends CardTestPlayerBase { + + private static final String drake = "Seacoast Drake"; // 1/3 flying + private static final String crab = "Jwari Scuttler"; // 2/3 + private static final String shinobi = "Moonblade Shinobi"; // 3/2, Ninjutsu 2U, on combat damage to player create 1/1 Illusion + + @Test + public void testMultipleUsage() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + addCard(Zone.BATTLEFIELD, playerA, drake); + addCard(Zone.BATTLEFIELD, playerA, crab); + addCard(Zone.HAND, playerA, shinobi); + + attack(1, playerA, drake, playerB); + attack(1, playerA, crab, playerB); + + activateAbility(1, PhaseStep.DECLARE_BLOCKERS, playerA, "Ninjutsu"); + setChoice(playerA, drake); + activateAbility(1, PhaseStep.DECLARE_BLOCKERS, playerA, "Ninjutsu"); // while above ability on stack + setChoice(playerA, crab); + // result, both drake and crab in hand, shinobi on battlefield + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_COMBAT); + execute(); + + assertPowerToughness(playerA, shinobi, 3, 2); + assertPermanentCount(playerA, "Illusion Token", 1); + assertHandCount(playerA, drake, 1); + assertHandCount(playerA, crab, 1); + assertTappedCount("Island", true, 6); // activated twice + } + +} diff --git a/Mage/src/main/java/mage/abilities/keyword/NinjutsuAbility.java b/Mage/src/main/java/mage/abilities/keyword/NinjutsuAbility.java index a897d732f4f..32422330d34 100644 --- a/Mage/src/main/java/mage/abilities/keyword/NinjutsuAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/NinjutsuAbility.java @@ -54,7 +54,7 @@ public class NinjutsuAbility extends ActivatedAbilityImpl { } public NinjutsuAbility(Cost cost, boolean commander) { - super(commander ? Zone.ALL : Zone.HAND, new NinjutsuEffect(), cost); + super(commander ? Zone.ALL : Zone.HAND, new NinjutsuEffect(commander), cost); this.addCost(new RevealNinjutsuCardCost(commander)); this.addCost(new ReturnAttackerToHandTargetCost()); this.commander = commander; @@ -84,14 +84,18 @@ public class NinjutsuAbility extends ActivatedAbilityImpl { class NinjutsuEffect extends OneShotEffect { - public NinjutsuEffect() { + private final boolean commander; + + NinjutsuEffect(boolean commander) { super(Outcome.PutCreatureInPlay); + this.commander = commander; this.staticText = "Put this card onto the battlefield " + "from your hand tapped and attacking"; } - protected NinjutsuEffect(final NinjutsuEffect effect) { + private NinjutsuEffect(final NinjutsuEffect effect) { super(effect); + this.commander = effect.commander; } @Override @@ -102,11 +106,12 @@ class NinjutsuEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller == null) { + Card card = game.getCard(source.getSourceId()); + if (controller == null || card == null) { return false; } - Card card = game.getCard(source.getSourceId()); - if (card != null) { + Zone cardZone = game.getState().getZone(card.getId()); + if (cardZone == Zone.HAND || (commander && cardZone == Zone.COMMAND)) { controller.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null); Permanent permanent = game.getPermanent(source.getSourceId()); if (permanent != null) { @@ -144,6 +149,7 @@ class ReturnAttackerToHandTargetCost extends CostImpl { public ReturnAttackerToHandTargetCost(ReturnAttackerToHandTargetCost cost) { super(cost); + this.defendingPlayerId = cost.defendingPlayerId; } @Override From 7b03af3de3a0709f404e2e2fe43a35bf06142184 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 15 Mar 2025 17:45:28 -0400 Subject: [PATCH 35/44] fix #13433 (DoIfCostPaid regression), add test --- .../cards/conditional/DoIfCostPaidTest.java | 27 +++++++++++++++++++ .../effects/common/DoIfCostPaid.java | 10 +++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/conditional/DoIfCostPaidTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/conditional/DoIfCostPaidTest.java index b24e05559eb..0df14fb489a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/conditional/DoIfCostPaidTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/conditional/DoIfCostPaidTest.java @@ -98,4 +98,31 @@ public class DoIfCostPaidTest extends CardTestPlayerBase { execute(); } + @Test + public void testCannotPay() { + String thirst = "Thirst for Meaning"; // Draw three cards. Then discard two cards unless you discard an enchantment card. + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Craw Wurm"); + addCard(Zone.LIBRARY, playerA, "Runeclaw Bear"); + addCard(Zone.LIBRARY, playerA, "Glory Seeker"); + addCard(Zone.HAND, playerA, thirst); + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, thirst); + // can't discard enchantment, so must discard two + setChoice(playerA, "Runeclaw Bear"); + setChoice(playerA, "Glory Seeker"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, 3); + assertGraveyardCount(playerA, thirst, 1); + assertGraveyardCount(playerA, "Runeclaw Bear", 1); + assertGraveyardCount(playerA, "Glory Seeker", 1); + assertHandCount(playerA, 1); + assertHandCount(playerA, "Craw Wurm", 1); + } + } diff --git a/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java b/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java index e577f5d4ea0..321c0ef7787 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java +++ b/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java @@ -98,16 +98,12 @@ public class DoIfCostPaid extends OneShotEffect { if (player == null || mageObject == null) { return false; } - - // nothing to pay (do not support mana cost - it's true all the time) - if (!this.cost.canPay(source, source, player.getId(), game)) { - return false; - } - String message = CardUtil.replaceSourceName(makeChooseText(game, source), mageObject.getName()); Outcome payOutcome = executingEffects.getOutcome(source, this.outcome); + // nothing to pay (do not support mana cost - it's true all the time) + boolean canPay = cost.canPay(source, source, player.getId(), game); boolean didPay = false; - if (!optional || player.chooseUse(payOutcome, message, source, game)) { + if (canPay && (!optional || player.chooseUse(payOutcome, message, source, game))) { cost.clearPaid(); int bookmark = game.bookmarkState(); if (cost.pay(source, game, source, player.getId(), false)) { From 4d1c6def2380d811c9fa741ffaaa4a849315d298 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 15 Mar 2025 18:02:03 -0400 Subject: [PATCH 36/44] fix Mongrel Pack, add test --- Mage.Sets/src/mage/cards/m/MongrelPack.java | 15 ++--- .../cards/single/tmp/MongrelPackTest.java | 67 +++++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MongrelPackTest.java diff --git a/Mage.Sets/src/mage/cards/m/MongrelPack.java b/Mage.Sets/src/mage/cards/m/MongrelPack.java index 30536fdb973..c138d3f181e 100644 --- a/Mage.Sets/src/mage/cards/m/MongrelPack.java +++ b/Mage.Sets/src/mage/cards/m/MongrelPack.java @@ -1,14 +1,13 @@ package mage.cards.m; import mage.MageInt; -import mage.abilities.common.ZoneChangeTriggeredAbility; +import mage.abilities.common.DiesSourceTriggeredAbility; import mage.abilities.effects.common.CreateTokenEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; import mage.constants.TurnPhase; -import mage.constants.Zone; import mage.game.Game; import mage.game.events.GameEvent; import mage.game.permanent.token.GreenDogToken; @@ -41,10 +40,11 @@ public final class MongrelPack extends CardImpl { } } -class MongrelPackAbility extends ZoneChangeTriggeredAbility { +class MongrelPackAbility extends DiesSourceTriggeredAbility { MongrelPackAbility() { - super(Zone.BATTLEFIELD, Zone.GRAVEYARD, new CreateTokenEffect(new GreenDogToken(), 4), "When {this} dies during combat, ", false); + super(new CreateTokenEffect(new GreenDogToken(), 4)); + setTriggerPhrase("When {this} dies during combat, "); } private MongrelPackAbility(MongrelPackAbility ability) { @@ -58,11 +58,6 @@ class MongrelPackAbility extends ZoneChangeTriggeredAbility { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (super.checkTrigger(event, game)) { - if (game.getTurnPhaseType() == TurnPhase.COMBAT) { - return true; - } - } - return false; + return game.getTurnPhaseType() == TurnPhase.COMBAT && super.checkTrigger(event, game); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MongrelPackTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MongrelPackTest.java new file mode 100644 index 00000000000..7444b55558a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MongrelPackTest.java @@ -0,0 +1,67 @@ +package org.mage.test.cards.single.tmp; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class MongrelPackTest extends CardTestPlayerBase { + + private static final String mp = "Mongrel Pack"; // 4/1 + // When this creature dies during combat, create four 1/1 green Dog creature tokens. + private static final String wurm = "Craw Wurm"; // 6/4 + private static final String bb = "Blood Bairn"; // sac outlet + + @Test + public void testDuringCombat() { + addCard(Zone.BATTLEFIELD, playerA, mp); + addCard(Zone.BATTLEFIELD, playerB, wurm); + + attack(1, playerA, mp, playerB); + block(1, playerB, wurm, mp); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, mp, 1); + assertGraveyardCount(playerB, wurm, 1); + assertPermanentCount(playerA, "Dog Token", 4); + } + + @Test + public void testNotDuringCombat() { + addCard(Zone.BATTLEFIELD, playerA, mp); + addCard(Zone.BATTLEFIELD, playerA, bb); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + setChoice(playerA, mp); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, mp, 1); + assertPermanentCount(playerA, "Dog Token", 0); + } + + @Test + public void testDuringCombatNotFromCombat() { + addCard(Zone.BATTLEFIELD, playerA, mp); + addCard(Zone.BATTLEFIELD, playerA, bb); + + activateAbility(1, PhaseStep.BEGIN_COMBAT, playerA, "Sacrifice"); + setChoice(playerA, mp); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, mp, 1); + assertPermanentCount(playerA, "Dog Token", 4); + } + +} From 7c55d444b08a09af9d10fd20e1ac84c2b6a2d1af Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Sat, 15 Mar 2025 19:17:44 -0500 Subject: [PATCH 37/44] [DFT] Implement Push the Limit (#13408) --- .../src/mage/cards/a/ArmedAndArmored.java | 39 +------ Mage.Sets/src/mage/cards/p/PushTheLimit.java | 108 ++++++++++++++++++ .../src/mage/cards/s/StartYourEngines.java | 39 +------ Mage.Sets/src/mage/sets/Aetherdrift.java | 1 + .../VehiclesBecomeArtifactCreatureEffect.java | 42 +++++++ 5 files changed, 155 insertions(+), 74 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/p/PushTheLimit.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/continuous/VehiclesBecomeArtifactCreatureEffect.java diff --git a/Mage.Sets/src/mage/cards/a/ArmedAndArmored.java b/Mage.Sets/src/mage/cards/a/ArmedAndArmored.java index 6dd15a380c7..614df035243 100644 --- a/Mage.Sets/src/mage/cards/a/ArmedAndArmored.java +++ b/Mage.Sets/src/mage/cards/a/ArmedAndArmored.java @@ -4,8 +4,8 @@ import java.util.List; import java.util.UUID; import mage.abilities.Ability; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.VehiclesBecomeArtifactCreatureEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -25,7 +25,7 @@ public final class ArmedAndArmored extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}"); // Vehicles you control become artifact creatures until end of turn. - this.getSpellAbility().addEffect(new ArmedAndArmoredEffect()); + this.getSpellAbility().addEffect(new VehiclesBecomeArtifactCreatureEffect(Duration.EndOfTurn)); // Choose a Dwarf you control. Attach any number of Equipment you control to it. this.getSpellAbility().addEffect(new ArmedAndArmoredEquipEffect()); @@ -41,41 +41,6 @@ public final class ArmedAndArmored extends CardImpl { } } -class ArmedAndArmoredEffect extends ContinuousEffectImpl { - - ArmedAndArmoredEffect() { - super(Duration.EndOfTurn, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.BecomeCreature); - staticText = "Vehicles you control become artifact creatures until end of turn"; - } - - private ArmedAndArmoredEffect(final ArmedAndArmoredEffect effect) { - super(effect); - } - - @Override - public ArmedAndArmoredEffect copy() { - return new ArmedAndArmoredEffect(this); - } - - @Override - public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(source.getControllerId())) { - if (permanent != null && permanent.hasSubtype(SubType.VEHICLE, game)) { - if (sublayer == SubLayer.NA) { - permanent.addCardType(game, CardType.ARTIFACT); - permanent.addCardType(game, CardType.CREATURE);// TODO: Check if giving CREATURE Type is correct - } - } - } - return true; - } - - @Override - public boolean apply(Game game, Ability source) { - return false; - } -} - class ArmedAndArmoredEquipEffect extends OneShotEffect { ArmedAndArmoredEquipEffect() { diff --git a/Mage.Sets/src/mage/cards/p/PushTheLimit.java b/Mage.Sets/src/mage/cards/p/PushTheLimit.java new file mode 100644 index 00000000000..1ee2960811f --- /dev/null +++ b/Mage.Sets/src/mage/cards/p/PushTheLimit.java @@ -0,0 +1,108 @@ +package mage.cards.p; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.*; +import mage.abilities.effects.common.continuous.BecomesSubtypeAllEffect; +import mage.abilities.effects.common.continuous.CreaturesBecomeOtherTypeEffect; +import mage.abilities.effects.common.continuous.GainAbilityAllEffect; +import mage.abilities.effects.common.continuous.VehiclesBecomeArtifactCreatureEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.MageObjectReferencePredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.targetpointer.FixedTargets; + +/** + * + * @author Jmlundeen + */ +public final class PushTheLimit extends CardImpl { + private static final FilterPermanent filterCreatures = new FilterControlledPermanent("Creatures you control"); + + public PushTheLimit(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{5}{R}{R}"); + + + // Return all Mount and Vehicle cards from your graveyard to the battlefield. Sacrifice them at the beginning of the next end step. + this.getSpellAbility().addEffect(new PushTheLimitEffect()); + // Vehicles you control become artifact creatures until end of turn. Creatures you control gain haste until end of turn. + this.getSpellAbility().addEffect(new VehiclesBecomeArtifactCreatureEffect(Duration.EndOfTurn) + .concatBy("
")); + this.getSpellAbility().addEffect(new GainAbilityAllEffect(HasteAbility.getInstance(), Duration.EndOfTurn, filterCreatures)); + } + + private PushTheLimit(final PushTheLimit card) { + super(card); + } + + @Override + public PushTheLimit copy() { + return new PushTheLimit(this); + } +} + +class PushTheLimitEffect extends OneShotEffect { + private static final FilterCard filter = new FilterCard("Mount and Vehicle cards"); + static { + filter.add(Predicates.or( + SubType.MOUNT.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + } + public PushTheLimitEffect() { + super(Outcome.PutCreatureInPlay); + staticText = "return all " + filter.getMessage() + " from your graveyard to the battlefield. " + + "Sacrifice them at the beginning of the next end step."; + } + + public PushTheLimitEffect(final PushTheLimitEffect effect) { + super(effect); + } + + @Override + public PushTheLimitEffect copy() { + return new PushTheLimitEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Set cards = controller.getGraveyard().getCards(filter, source.getControllerId(), source, game); + boolean result = controller.moveCards(cards, Zone.BATTLEFIELD, source, game, + false, false, false, null); + if (result) { + List permanentsToSac = cards.stream() + .map(card -> game.getPermanent(card.getId())) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + Effect sacrificeEffect = new SacrificeTargetEffect("sacrifice them", source.getControllerId()); + sacrificeEffect.setTargetPointer(new FixedTargets(permanentsToSac, game)); + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(sacrificeEffect), source); + } + return result; + } +} diff --git a/Mage.Sets/src/mage/cards/s/StartYourEngines.java b/Mage.Sets/src/mage/cards/s/StartYourEngines.java index a14c7ede336..bba91ec4f90 100644 --- a/Mage.Sets/src/mage/cards/s/StartYourEngines.java +++ b/Mage.Sets/src/mage/cards/s/StartYourEngines.java @@ -6,6 +6,7 @@ import mage.abilities.Ability; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.VehiclesBecomeArtifactCreatureEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -22,7 +23,7 @@ public final class StartYourEngines extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{R}"); // Vehicles you control becomes artifact creatures until end of turn. - Effect effect = new StartYourEnginesEffect(); + Effect effect = new VehiclesBecomeArtifactCreatureEffect(Duration.EndOfTurn); this.getSpellAbility().addEffect(effect); // Creatures you control get +2/+0 until end of turn. @@ -38,39 +39,3 @@ public final class StartYourEngines extends CardImpl { return new StartYourEngines(this); } } - -class StartYourEnginesEffect extends ContinuousEffectImpl { - - StartYourEnginesEffect() { - super(Duration.EndOfTurn, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.BecomeCreature); - staticText = "Vehicles you control become artifact creatures until end of turn"; - } - - private StartYourEnginesEffect(final StartYourEnginesEffect effect) { - super(effect); - } - - @Override - public StartYourEnginesEffect copy() { - return new StartYourEnginesEffect(this); - } - - @Override - public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { - for (Permanent permanent : game.getBattlefield().getAllActivePermanents(source.getControllerId())) { - if (permanent != null && permanent.hasSubtype(SubType.VEHICLE, game)) { - if (sublayer == SubLayer.NA) { - permanent.addCardType(game, CardType.ARTIFACT); - permanent.addCardType(game, CardType.CREATURE);// TODO: Check if giving CREATURE Type is correct - } - } - } - return true; - } - - @Override - public boolean apply(Game game, Ability source) { - return false; - } - -} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 5e2dbd77c71..b3174d15646 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -214,6 +214,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Pothole Mole", 176, Rarity.COMMON, mage.cards.p.PotholeMole.class)); cards.add(new SetCardInfo("Pride of the Road", 24, Rarity.UNCOMMON, mage.cards.p.PrideOfTheRoad.class)); cards.add(new SetCardInfo("Prowcatcher Specialist", 142, Rarity.COMMON, mage.cards.p.ProwcatcherSpecialist.class)); + cards.add(new SetCardInfo("Push the Limit", 143, Rarity.UNCOMMON, mage.cards.p.PushTheLimit.class)); cards.add(new SetCardInfo("Pyrewood Gearhulk", 216, Rarity.MYTHIC, mage.cards.p.PyrewoodGearhulk.class)); cards.add(new SetCardInfo("Quag Feast", 100, Rarity.RARE, mage.cards.q.QuagFeast.class)); cards.add(new SetCardInfo("Racers' Scoreboard", 239, Rarity.UNCOMMON, mage.cards.r.RacersScoreboard.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/VehiclesBecomeArtifactCreatureEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/VehiclesBecomeArtifactCreatureEffect.java new file mode 100644 index 00000000000..961adf11310 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/VehiclesBecomeArtifactCreatureEffect.java @@ -0,0 +1,42 @@ +package mage.abilities.effects.common.continuous; + +import mage.abilities.Ability; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.constants.*; +import mage.game.Game; +import mage.game.permanent.Permanent; + +public class VehiclesBecomeArtifactCreatureEffect extends ContinuousEffectImpl { + + public VehiclesBecomeArtifactCreatureEffect(Duration duration) { + super(duration, Layer.TypeChangingEffects_4, SubLayer.NA, Outcome.BecomeCreature); + staticText = "Vehicles you control become artifact creatures until end of turn"; + } + + private VehiclesBecomeArtifactCreatureEffect(final VehiclesBecomeArtifactCreatureEffect effect) { + super(effect); + } + + @Override + public VehiclesBecomeArtifactCreatureEffect copy() { + return new VehiclesBecomeArtifactCreatureEffect(this); + } + + @Override + public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { + for (Permanent permanent : game.getBattlefield().getAllActivePermanents(source.getControllerId())) { + if (permanent != null && permanent.hasSubtype(SubType.VEHICLE, game)) { + if (sublayer == SubLayer.NA) { + permanent.addCardType(game, CardType.ARTIFACT); + permanent.addCardType(game, CardType.CREATURE);// TODO: Check if giving CREATURE Type is correct + } + } + } + return true; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } +} From 66fd5c1b6ac2e3537a408bcd97ad46b1d14c6849 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Sat, 15 Mar 2025 19:21:58 -0500 Subject: [PATCH 38/44] [DFT] Implement many cards with custom effects (#13407) * [DFT] Implement Loot, the Pathfinder add option to hide reminder text for exhaust ability * [DFT] Implement Guidelight Optimizer * [DFT] Implement Radiant Lotus * [DFT] Implement Oildeep Gearhulk * fix Oildeep Gearhulk target * fix OilDeep Gearhulk duplicate hand reveal * [DFT] Implement Momentum Breaker * [DFT] Implement Sita Varma, Masked Racer * [DFT] Implement SkySeers Chariot * [DFT] Implement Skyserpent Seeker * [DFT] Implement Tune Up * fix Skyseer's Chariot modifying spell cost * use exhaust constructor boolean for reminderText * Update cards for review change radiant lotus outcome to prevent AI from trying to use it change oildeep card choice to discard replace Composite cost and move discard effect if sacrifice was unsuccessful replace Composite costs and add target to Loot's third ability * Missed braces for mana cost Update GuidelightOptimizer text --- .../src/mage/cards/g/GuidelightOptimizer.java | 83 +++++++++++++++ .../src/mage/cards/l/LootThePathfinder.java | 72 +++++++++++++ .../src/mage/cards/m/MomentumBreaker.java | 100 ++++++++++++++++++ .../src/mage/cards/o/OildeepGearhulk.java | 97 +++++++++++++++++ Mage.Sets/src/mage/cards/r/RadiantLotus.java | 88 +++++++++++++++ .../mage/cards/s/SitaVarmaMaskedRacer.java | 87 +++++++++++++++ .../src/mage/cards/s/SkyseersChariot.java | 90 ++++++++++++++++ .../src/mage/cards/s/SkyserpentSeeker.java | 95 +++++++++++++++++ Mage.Sets/src/mage/cards/t/TuneUp.java | 70 ++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 27 +++++ .../abilities/keyword/ExhaustAbility.java | 17 ++- 11 files changed, 825 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/g/GuidelightOptimizer.java create mode 100644 Mage.Sets/src/mage/cards/l/LootThePathfinder.java create mode 100644 Mage.Sets/src/mage/cards/m/MomentumBreaker.java create mode 100644 Mage.Sets/src/mage/cards/o/OildeepGearhulk.java create mode 100644 Mage.Sets/src/mage/cards/r/RadiantLotus.java create mode 100644 Mage.Sets/src/mage/cards/s/SitaVarmaMaskedRacer.java create mode 100644 Mage.Sets/src/mage/cards/s/SkyseersChariot.java create mode 100644 Mage.Sets/src/mage/cards/s/SkyserpentSeeker.java create mode 100644 Mage.Sets/src/mage/cards/t/TuneUp.java diff --git a/Mage.Sets/src/mage/cards/g/GuidelightOptimizer.java b/Mage.Sets/src/mage/cards/g/GuidelightOptimizer.java new file mode 100644 index 00000000000..344860c6e58 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GuidelightOptimizer.java @@ -0,0 +1,83 @@ +package mage.cards.g; + +import java.util.UUID; + +import mage.ConditionalMana; +import mage.MageInt; +import mage.MageObject; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.mana.ConditionalColoredManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.abilities.mana.conditional.ManaCondition; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.Game; + +/** + * + * @author Jmlundeen + */ +public final class GuidelightOptimizer extends CardImpl { + + public GuidelightOptimizer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.ROBOT); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // {T}: Add {U}. Spend this mana only to cast an artifact spell or activate an ability. + this.addAbility(new ConditionalColoredManaAbility(Mana.BlueMana(1), new ArtifactOrActivatedManaBuilder())); + } + + private GuidelightOptimizer(final GuidelightOptimizer card) { + super(card); + } + + @Override + public GuidelightOptimizer copy() { + return new GuidelightOptimizer(this); + } +} + +class ArtifactOrActivatedManaBuilder extends ConditionalManaBuilder { + + @Override + public ConditionalMana build(Object... options) { + return new ArtifactOrActivatedConditionalMana(this.mana); + } + + @Override + public String getRule() { + return "Spend this mana only to cast an artifact spells or activate an ability"; + } +} + +class ArtifactOrActivatedConditionalMana extends ConditionalMana { + + public ArtifactOrActivatedConditionalMana(Mana mana) { + super(mana); + staticText = "Spend this mana only to cast an artifact spells or activate an ability"; + addCondition(new ArtifactOrActivatedManaCondition()); + } +} + +class ArtifactOrActivatedManaCondition extends ManaCondition implements Condition { + + @Override + public boolean apply(Game game, Ability source) { + MageObject sourceObject = game.getObject(source); + return source != null + && (source.isActivatedAbility() && !source.isActivated()) + || (sourceObject != null && sourceObject.isArtifact()); + } + + @Override + public boolean apply(Game game, Ability source, UUID originalId, mage.abilities.costs.Cost costsToPay) { + return apply(game, source); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/l/LootThePathfinder.java b/Mage.Sets/src/mage/cards/l/LootThePathfinder.java new file mode 100644 index 00000000000..3aac0c78590 --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LootThePathfinder.java @@ -0,0 +1,72 @@ +package mage.cards.l; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.costs.CompositeCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.mana.AddManaOfAnyColorEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.common.TargetAnyTarget; + +/** + * + * @author Jmlundeen + */ +public final class LootThePathfinder extends CardImpl { + + public LootThePathfinder(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.BEAST); + this.subtype.add(SubType.NOBLE); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Double strike + this.addAbility(DoubleStrikeAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // Exhaust -- {G}, {T}: Add three mana of any one color. + Ability abilityOne = new ExhaustAbility(new AddManaOfAnyColorEffect(3), new ManaCostsImpl<>("{G}")); + abilityOne.addCost(new TapSourceCost()); + this.addAbility(abilityOne); + + // Exhaust -- {U}, {T}: Draw three cards. + Ability abilityTwo = new ExhaustAbility(new DrawCardSourceControllerEffect(3), new ManaCostsImpl<>("{U}"), false); + abilityTwo.addCost(new TapSourceCost()); + this.addAbility(abilityTwo); + + // Exhaust -- {R}, {T}: Loot deals 3 damage to any target. + Ability abilityThree = new ExhaustAbility(new DamageTargetEffect(3), new ManaCostsImpl<>("{R}"), false); + abilityThree.addCost(new TapSourceCost()); + abilityThree.addTarget(new TargetAnyTarget()); + this.addAbility(abilityThree); + } + + private LootThePathfinder(final LootThePathfinder card) { + super(card); + } + + @Override + public LootThePathfinder copy() { + return new LootThePathfinder(this); + } +} diff --git a/Mage.Sets/src/mage/cards/m/MomentumBreaker.java b/Mage.Sets/src/mage/cards/m/MomentumBreaker.java new file mode 100644 index 00000000000..44b10d70ba1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MomentumBreaker.java @@ -0,0 +1,100 @@ +package mage.cards.m; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SacrificeSourceTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.CompositeCost; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.ControllerSpeedCount; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.SacrificeEffect; +import mage.abilities.keyword.StartYourEnginesAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.target.targetpointer.FixedTarget; + +/** + * + * @author Jmlundeen + */ +public final class MomentumBreaker extends CardImpl { + + public MomentumBreaker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}"); + + + // Start your engines! + this.addAbility(new StartYourEnginesAbility()); + + // When this enchantment enters, each opponent sacrifices a creature or Vehicle of their choice. Each opponent who can't discards a card. + this.addAbility(new EntersBattlefieldTriggeredAbility(new MomentumBreakerEffect())); + // {2}, Sacrifice this enchantment: You gain life equal to your speed. + Effect gainLifeEffect = new GainLifeEffect(ControllerSpeedCount.instance).setText("You gain life equal to your speed"); + Ability ability = new SimpleActivatedAbility(gainLifeEffect,new ManaCostsImpl<>("{2}")); + ability.addCost(new SacrificeSourceCost()); + this.addAbility(ability); + } + + private MomentumBreaker(final MomentumBreaker card) { + super(card); + } + + @Override + public MomentumBreaker copy() { + return new MomentumBreaker(this); + } +} + +class MomentumBreakerEffect extends OneShotEffect { + + MomentumBreakerEffect() { + super(Outcome.Benefit); + this.staticText = "each opponent sacrifices a creature or Vehicle of their choice. " + + "Each opponent who can't discards a card."; + } + + private MomentumBreakerEffect(final MomentumBreakerEffect effect) { + super(effect); + } + + @Override + public MomentumBreakerEffect copy() { + return new MomentumBreakerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + game.getOpponents(source.getControllerId()).forEach(opponent -> { + Player player = game.getPlayer(opponent); + if (player != null) { + FilterPermanent filter = new FilterPermanent("creature or Vehicle"); + filter.add(Predicates.or( + CardType.CREATURE.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + filter.add(new ControllerIdPredicate(opponent)); + Effect effect = new SacrificeEffect(filter, 1, null); + effect.setTargetPointer(new FixedTarget(player.getId(), game)); + if (!effect.apply(game, source)) { + player.discard(1, false, false, source, game); + } + } + }); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/o/OildeepGearhulk.java b/Mage.Sets/src/mage/cards/o/OildeepGearhulk.java new file mode 100644 index 00000000000..8a6d151bf83 --- /dev/null +++ b/Mage.Sets/src/mage/cards/o/OildeepGearhulk.java @@ -0,0 +1,97 @@ +package mage.cards.o; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawDiscardTargetEffect; +import mage.abilities.effects.common.LookAtTargetPlayerHandEffect; +import mage.abilities.effects.common.discard.DiscardAndDrawThatManyEffect; +import mage.cards.Card; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.abilities.keyword.LifelinkAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.TargetPlayer; +import mage.target.common.TargetCardInHand; + +/** + * + * @author Jmlundeen + */ +public final class OildeepGearhulk extends CardImpl { + + public OildeepGearhulk(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{U}{U}{B}{B}"); + + this.subtype.add(SubType.CONSTRUCT); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Lifelink + this.addAbility(LifelinkAbility.getInstance()); + + // Ward {1} + this.addAbility(new WardAbility(new ManaCostsImpl<>("{1}"))); + + // When this creature enters, look at target player's hand. You may choose a card from it. If you do, that player discards that card, then draws a card. + Ability ability = new EntersBattlefieldTriggeredAbility(new OildeepGearhulkEffect()); + ability.addTarget(new TargetPlayer()); + this.addAbility(ability); + } + + private OildeepGearhulk(final OildeepGearhulk card) { + super(card); + } + + @Override + public OildeepGearhulk copy() { + return new OildeepGearhulk(this); + } +} + +class OildeepGearhulkEffect extends OneShotEffect { + + public OildeepGearhulkEffect() { + super(Outcome.Benefit); + staticText = "look at target player's hand. You may choose a card from it. If you do, that player discards that card, then draws a card"; + } + + public OildeepGearhulkEffect(final OildeepGearhulkEffect effect) { + super(effect); + } + + @Override + public OildeepGearhulkEffect copy() { + return new OildeepGearhulkEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + Player targetPlayer = game.getPlayer(source.getFirstTarget()); + if (controller == null || targetPlayer == null) { + return false; + } + + controller.lookAtCards(targetPlayer.getName() + " Hand", targetPlayer.getHand(), game); + TargetCard chosenCard = new TargetCardInHand(0, 1, new FilterCard("card to discard")); + if (controller.choose(Outcome.Discard, targetPlayer.getHand(), chosenCard, source, game)) { + Card card = game.getCard(chosenCard.getFirstTarget()); + targetPlayer.discard(card, false, source, game); + targetPlayer.drawCards(1, source, game); + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/cards/r/RadiantLotus.java b/Mage.Sets/src/mage/cards/r/RadiantLotus.java new file mode 100644 index 00000000000..61d85574693 --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RadiantLotus.java @@ -0,0 +1,88 @@ +package mage.cards.r; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.common.SacrificeXTargetCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.choices.ChoiceColor; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledArtifactPermanent; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPlayer; + +/** + * + * @author Jmlundeen + */ +public final class RadiantLotus extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledArtifactPermanent("artifacts"); + public RadiantLotus(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{6}"); + + + // {T}, Sacrifice one or more artifacts: Choose a color. Target player adds three mana of the chosen color for each artifact sacrificed this way. + Ability ability = new SimpleActivatedAbility(new RadiantLotusEffect(), new TapSourceCost()); + ability.addCost(new SacrificeXTargetCost(filter, true, 1).setText("Sacrifice one or more artifacts")); + ability.addTarget(new TargetPlayer()); + this.addAbility(ability); + } + + private RadiantLotus(final RadiantLotus card) { + super(card); + } + + @Override + public RadiantLotus copy() { + return new RadiantLotus(this); + } +} + + +class RadiantLotusEffect extends OneShotEffect { + + public RadiantLotusEffect() { + super(Outcome.AIDontUseIt); + staticText = "Choose a color. Target player adds three mana of the chosen color for each artifact sacrificed this way"; + } + + public RadiantLotusEffect(final RadiantLotusEffect effect) { + super(effect); + } + + @Override + public RadiantLotusEffect copy() { + return new RadiantLotusEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + int manaCount = source.getCosts().stream() + .filter(SacrificeTargetCost.class::isInstance) + .mapToInt(cost -> ((SacrificeTargetCost) cost).getPermanents().size() * 3) + .sum(); + + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + String mes = String.format("Select a color of mana to add %d of it", manaCount); + ChoiceColor choice = new ChoiceColor(true, mes, game.getObject(source)); + if (controller.choose(outcome, choice, game)) { + Player player = game.getPlayer(source.getFirstTarget()); + if (choice.getColor() != null && player != null) { + player.getManaPool().addMana(choice.getMana(manaCount), game, source); + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/SitaVarmaMaskedRacer.java b/Mage.Sets/src/mage/cards/s/SitaVarmaMaskedRacer.java new file mode 100644 index 00000000000..7ce6b9b1c6c --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SitaVarmaMaskedRacer.java @@ -0,0 +1,87 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.costs.Cost; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.dynamicvalue.common.SourcePermanentPowerValue; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.SetBasePowerToughnessAllEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; + +/** + * + * @author Jmlundeen + */ +public final class SitaVarmaMaskedRacer extends CardImpl { + + public SitaVarmaMaskedRacer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Exhaust -- {X}{G}{G}{U}: Put X +1/+1 counters on Sita Varma. Then you may have the base power and toughness of each other creature you control become equal to Sita Varma's power until end of turn. + Cost cost = new ManaCostsImpl<>("{X}{G}{G}{U}"); + Effect effect = new AddCountersSourceEffect(CounterType.P1P1.createInstance(), GetXValue.instance); + Ability ability = new ExhaustAbility(effect, cost); + ability.addEffect(new SitaVarmaMaskedRacerMayEffect()); + this.addAbility(ability); + } + + private SitaVarmaMaskedRacer(final SitaVarmaMaskedRacer card) { + super(card); + } + + @Override + public SitaVarmaMaskedRacer copy() { + return new SitaVarmaMaskedRacer(this); + } +} + +class SitaVarmaMaskedRacerMayEffect extends OneShotEffect { + private static final String choiceText = "Have the base power and toughness of each other creature you control" + + " become equal to Sita Varma's power until end of turn?"; + + public SitaVarmaMaskedRacerMayEffect() { + super(Outcome.Benefit); + this.staticText = "Then you may have the base power and toughness of each other creature you control become equal to Sita Varma's power until end of turn."; + } + + public SitaVarmaMaskedRacerMayEffect(final SitaVarmaMaskedRacerMayEffect effect) { + super(effect); + } + + @Override + public SitaVarmaMaskedRacerMayEffect copy() { + return new SitaVarmaMaskedRacerMayEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null || !controller.chooseUse(outcome, choiceText, source, game)) { + return false; + } + ContinuousEffect setBasePowerToughnessEffect = new SetBasePowerToughnessAllEffect(SourcePermanentPowerValue.ALLOW_NEGATIVE, + SourcePermanentPowerValue.ALLOW_NEGATIVE, Duration.EndOfTurn, + StaticFilters.FILTER_OTHER_CONTROLLED_CREATURES); + game.addEffect(setBasePowerToughnessEffect, source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/s/SkyseersChariot.java b/Mage.Sets/src/mage/cards/s/SkyseersChariot.java new file mode 100644 index 00000000000..dc1c73b37cf --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SkyseersChariot.java @@ -0,0 +1,90 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.AsEntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.ChooseACardNameEffect; +import mage.abilities.effects.common.cost.CostModificationEffectImpl; +import mage.abilities.effects.common.cost.SpellsCostIncreasingAllEffect; +import mage.constants.*; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.CrewAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.game.Game; +import mage.util.CardUtil; + +/** + * + * @author Jmlundeen + */ +public final class SkyseersChariot extends CardImpl { + + public SkyseersChariot(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{W}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // As this Vehicle enters, choose a nonland card name. + this.addAbility(new AsEntersBattlefieldAbility(new ChooseACardNameEffect(ChooseACardNameEffect.TypeOfName.NON_LAND_NAME))); + // Activated abilities of sources with the chosen name cost {2} more to activate. + this.addAbility(new SimpleStaticAbility(new SkyseersChariotEffect())); + + // Crew 2 + this.addAbility(new CrewAbility(2)); + + } + + private SkyseersChariot(final SkyseersChariot card) { + super(card); + } + + @Override + public SkyseersChariot copy() { + return new SkyseersChariot(this); + } +} + +class SkyseersChariotEffect extends CostModificationEffectImpl { + + SkyseersChariotEffect() { + super(Duration.WhileOnBattlefield, Outcome.Detriment, CostModificationType.INCREASE_COST); + staticText = "Activated abilities of sources with the chosen name cost {2} more to activate"; + } + + private SkyseersChariotEffect(final SkyseersChariotEffect effect) { + super(effect); + } + + @Override + public SkyseersChariotEffect copy() { + return new SkyseersChariotEffect(this); + } + + @Override + public boolean apply(Game game, Ability source, Ability abilityToModify) { + CardUtil.increaseCost(abilityToModify, 2); + return true; + } + + @Override + public boolean applies(Ability abilityToModify, Ability source, Game game) { + MageObject activatedSource = game.getObject(abilityToModify.getSourceId()); + if (activatedSource == null) { + return false; + } + String chosenName = (String) game.getState().getValue( + source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY + ); + return CardUtil.haveSameNames(activatedSource, chosenName, game) + && abilityToModify.isActivatedAbility(); + } +} diff --git a/Mage.Sets/src/mage/cards/s/SkyserpentSeeker.java b/Mage.Sets/src/mage/cards/s/SkyserpentSeeker.java new file mode 100644 index 00000000000..6f124c25c80 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SkyserpentSeeker.java @@ -0,0 +1,95 @@ +package mage.cards.s; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.RevealCardsFromLibraryUntilEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.ExhaustAbility; +import mage.cards.*; +import mage.constants.*; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.DeathtouchAbility; +import mage.counters.CounterType; +import mage.game.Game; +import mage.players.Player; + +/** + * + * @author Jmlundeen + */ +public final class SkyserpentSeeker extends CardImpl { + + public SkyserpentSeeker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}{U}"); + + this.subtype.add(SubType.SNAKE); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // Exhaust -- {4}: Reveal cards from the top of your library until you reveal two land cards. Put those land cards onto the battlefield tapped and the rest on the bottom of your library in a random order. Put a +1/+1 counter on this creature. + Ability ability = (new ExhaustAbility(new SkyserpentSeekerEffect(), new ManaCostsImpl<>("{4}"))); + ability.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(), StaticValue.get(1))); + this.addAbility(ability); + } + + private SkyserpentSeeker(final SkyserpentSeeker card) { + super(card); + } + + @Override + public SkyserpentSeeker copy() { + return new SkyserpentSeeker(this); + } + +} +class SkyserpentSeekerEffect extends OneShotEffect { + + SkyserpentSeekerEffect() { + super(Outcome.PutLandInPlay); + this.staticText = "Reveal cards from the top of your library until you reveal two land cards. " + + "Put those land cards onto the battlefield tapped and the rest on the bottom of your library in a random order"; + } + + private SkyserpentSeekerEffect(final SkyserpentSeekerEffect effect) { + super(effect); + } + + @Override + public SkyserpentSeekerEffect copy() { + return new SkyserpentSeekerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + Cards revealedCards = new CardsImpl(); + Cards lands = new CardsImpl(); + for (Card card : controller.getLibrary().getCards(game)) { + if (card.isLand()) { + lands.add(card); + if (lands.size() == 2) { + break; + } + } + revealedCards.add(card); + } + controller.revealCards(source, revealedCards, game); + PutCards.BATTLEFIELD_TAPPED.moveCards(controller, lands, source, game); + PutCards.BOTTOM_RANDOM.moveCards(controller, revealedCards, source, game); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/t/TuneUp.java b/Mage.Sets/src/mage/cards/t/TuneUp.java new file mode 100644 index 00000000000..eebcef11ae2 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TuneUp.java @@ -0,0 +1,70 @@ +package mage.cards.t; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.effects.common.ReturnFromYourGraveyardToBattlefieldAllEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCardInYourGraveyard; + +/** + * + * @author Jmlundeen + */ +public final class TuneUp extends CardImpl { + + public TuneUp(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{W}"); + + + // Return target artifact card from your graveyard to the battlefield. If it's a Vehicle, it becomes an artifact creature. + this.getSpellAbility().addEffect(new ReturnFromGraveyardToBattlefieldTargetEffect()); + this.getSpellAbility().addEffect(new TuneUpEffect()); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_ARTIFACT_FROM_YOUR_GRAVEYARD)); + } + + private TuneUp(final TuneUp card) { + super(card); + } + + @Override + public TuneUp copy() { + return new TuneUp(this); + } +} + +class TuneUpEffect extends OneShotEffect { + + TuneUpEffect() { + super(Outcome.BecomeCreature); + this.staticText = "If it's a Vehicle, it becomes an artifact creature"; + } + + private TuneUpEffect(final TuneUpEffect effect) { + super(effect); + } + + @Override + public TuneUpEffect copy() { + return new TuneUpEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent != null && permanent.hasSubtype(SubType.VEHICLE, game)) { + permanent.addCardType(CardType.CREATURE); + return true; + } + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index b3174d15646..c02b0043b70 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -141,6 +141,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Grim Bauble", 88, Rarity.COMMON, mage.cards.g.GrimBauble.class)); cards.add(new SetCardInfo("Grim Javelineer", 89, Rarity.COMMON, mage.cards.g.GrimJavelineer.class)); cards.add(new SetCardInfo("Guardian Sunmare", 15, Rarity.RARE, mage.cards.g.GuardianSunmare.class)); + cards.add(new SetCardInfo("Guidelight Optimizer", 45, Rarity.COMMON, mage.cards.g.GuidelightOptimizer.class)); cards.add(new SetCardInfo("Guidelight Pathmaker", 206, Rarity.UNCOMMON, mage.cards.g.GuidelightPathmaker.class)); cards.add(new SetCardInfo("Guidelight Synergist", 16, Rarity.UNCOMMON, mage.cards.g.GuidelightSynergist.class)); cards.add(new SetCardInfo("Haunt the Network", 207, Rarity.UNCOMMON, mage.cards.h.HauntTheNetwork.class)); @@ -170,6 +171,11 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Lightshield Parry", 19, Rarity.COMMON, mage.cards.l.LightshieldParry.class)); cards.add(new SetCardInfo("Lightwheel Enhancements", 20, Rarity.COMMON, mage.cards.l.LightwheelEnhancements.class)); cards.add(new SetCardInfo("Locust Spray", 95, Rarity.UNCOMMON, mage.cards.l.LocustSpray.class)); + cards.add(new SetCardInfo("Loot, the Pathfinder", 212, Rarity.MYTHIC, mage.cards.l.LootThePathfinder.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Loot, the Pathfinder", 391, Rarity.MYTHIC, mage.cards.l.LootThePathfinder.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Loot, the Pathfinder", 404, Rarity.MYTHIC, mage.cards.l.LootThePathfinder.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Loot, the Pathfinder", 414, Rarity.MYTHIC, mage.cards.l.LootThePathfinder.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Loot, the Pathfinder", 484, Rarity.MYTHIC, mage.cards.l.LootThePathfinder.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Lotusguard Disciple", 21, Rarity.COMMON, mage.cards.l.LotusguardDisciple.class)); cards.add(new SetCardInfo("Loxodon Surveyor", 167, Rarity.COMMON, mage.cards.l.LoxodonSurveyor.class)); cards.add(new SetCardInfo("Lumbering Worldwagon", 168, Rarity.RARE, mage.cards.l.LumberingWorldwagon.class)); @@ -191,6 +197,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Migrating Ketradon", 170, Rarity.COMMON, mage.cards.m.MigratingKetradon.class)); cards.add(new SetCardInfo("Mindspring Merfolk", 51, Rarity.RARE, mage.cards.m.MindspringMerfolk.class)); cards.add(new SetCardInfo("Molt Tender", 171, Rarity.UNCOMMON, mage.cards.m.MoltTender.class)); + cards.add(new SetCardInfo("Momentum Breaker", 97, Rarity.UNCOMMON, mage.cards.m.MomentumBreaker.class)); cards.add(new SetCardInfo("Monument to Endurance", 237, Rarity.RARE, mage.cards.m.MonumentToEndurance.class)); cards.add(new SetCardInfo("Mountain", 286, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Mu Yanling, Wind Rider", 52, Rarity.MYTHIC, mage.cards.m.MuYanlingWindRider.class)); @@ -199,6 +206,10 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Nesting Bot", 22, Rarity.UNCOMMON, mage.cards.n.NestingBot.class)); cards.add(new SetCardInfo("Night Market", 258, Rarity.COMMON, mage.cards.n.NightMarket.class)); cards.add(new SetCardInfo("Nimble Thopterist", 53, Rarity.COMMON, mage.cards.n.NimbleThopterist.class)); + cards.add(new SetCardInfo("Oildeep Gearhulk", 215, Rarity.MYTHIC, mage.cards.o.OildeepGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Oildeep Gearhulk", 351, Rarity.MYTHIC, mage.cards.o.OildeepGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Oildeep Gearhulk", 487, Rarity.MYTHIC, mage.cards.o.OildeepGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Oildeep Gearhulk", 550, Rarity.MYTHIC, mage.cards.o.OildeepGearhulk.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Ooze Patrol", 172, Rarity.UNCOMMON, mage.cards.o.OozePatrol.class)); cards.add(new SetCardInfo("Outpace Oblivion", 139, Rarity.UNCOMMON, mage.cards.o.OutpaceOblivion.class)); cards.add(new SetCardInfo("Oviya, Automech Artisan", 173, Rarity.RARE, mage.cards.o.OviyaAutomechArtisan.class)); @@ -218,6 +229,11 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Pyrewood Gearhulk", 216, Rarity.MYTHIC, mage.cards.p.PyrewoodGearhulk.class)); cards.add(new SetCardInfo("Quag Feast", 100, Rarity.RARE, mage.cards.q.QuagFeast.class)); cards.add(new SetCardInfo("Racers' Scoreboard", 239, Rarity.UNCOMMON, mage.cards.r.RacersScoreboard.class)); + cards.add(new SetCardInfo("Radiant Lotus", 240, Rarity.MYTHIC, mage.cards.r.RadiantLotus.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Radiant Lotus", 395, Rarity.MYTHIC, mage.cards.r.RadiantLotus.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Radiant Lotus", 406, Rarity.MYTHIC, mage.cards.r.RadiantLotus.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Radiant Lotus", 416, Rarity.MYTHIC, mage.cards.r.RadiantLotus.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Radiant Lotus", 500, Rarity.MYTHIC, mage.cards.r.RadiantLotus.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Rangers' Aetherhive", 217, Rarity.UNCOMMON, mage.cards.r.RangersAetherhive.class)); cards.add(new SetCardInfo("Rangers' Refueler", 55, Rarity.UNCOMMON, mage.cards.r.RangersRefueler.class)); cards.add(new SetCardInfo("Reckless Velocitaur", 144, Rarity.UNCOMMON, mage.cards.r.RecklessVelocitaur.class)); @@ -255,8 +271,17 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Scrounging Skyray", 60, Rarity.UNCOMMON, mage.cards.s.ScroungingSkyray.class)); cards.add(new SetCardInfo("Shefet Archfiend", 104, Rarity.UNCOMMON, mage.cards.s.ShefetArchfiend.class)); cards.add(new SetCardInfo("Silken Strength", 180, Rarity.COMMON, mage.cards.s.SilkenStrength.class)); + cards.add(new SetCardInfo("Sita Varma, Masked Racer", 223, Rarity.RARE, mage.cards.s.SitaVarmaMaskedRacer.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sita Varma, Masked Racer", 368, Rarity.RARE, mage.cards.s.SitaVarmaMaskedRacer.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sita Varma, Masked Racer", 493, Rarity.RARE, mage.cards.s.SitaVarmaMaskedRacer.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Skybox Ferry", 243, Rarity.COMMON, mage.cards.s.SkyboxFerry.class)); cards.add(new SetCardInfo("Skycrash", 146, Rarity.UNCOMMON, mage.cards.s.Skycrash.class)); + cards.add(new SetCardInfo("Skyseer's Chariot", 28, Rarity.RARE, mage.cards.s.SkyseersChariot.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Skyseer's Chariot", 296, Rarity.RARE, mage.cards.s.SkyseersChariot.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Skyseer's Chariot", 432, Rarity.RARE, mage.cards.s.SkyseersChariot.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Skyseer's Chariot", 518, Rarity.RARE, mage.cards.s.SkyseersChariot.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Skyserpent Seeker", 224, Rarity.UNCOMMON, mage.cards.s.SkyserpentSeeker.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Skyserpent Seeker", 420, Rarity.UNCOMMON, mage.cards.s.SkyserpentSeeker.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Skystreak Engineer", 61, Rarity.COMMON, mage.cards.s.SkystreakEngineer.class)); cards.add(new SetCardInfo("Slick Imitator", 62, Rarity.UNCOMMON, mage.cards.s.SlickImitator.class)); cards.add(new SetCardInfo("Spectacular Pileup", 29, Rarity.RARE, mage.cards.s.SpectacularPileup.class, NON_FULL_USE_VARIOUS)); @@ -299,6 +324,8 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Tranquil Cove", 267, Rarity.COMMON, mage.cards.t.TranquilCove.class)); cards.add(new SetCardInfo("Transit Mage", 70, Rarity.UNCOMMON, mage.cards.t.TransitMage.class)); cards.add(new SetCardInfo("Trip Up", 71, Rarity.COMMON, mage.cards.t.TripUp.class)); + cards.add(new SetCardInfo("Tune Up", 33, Rarity.UNCOMMON, mage.cards.t.TuneUp.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Tune Up", 417, Rarity.UNCOMMON, mage.cards.t.TuneUp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Tyrox, Saurid Tyrant", 149, Rarity.UNCOMMON, mage.cards.t.TyroxSauridTyrant.class)); cards.add(new SetCardInfo("Unstoppable Plan", 72, Rarity.RARE, mage.cards.u.UnstoppablePlan.class)); cards.add(new SetCardInfo("Unswerving Sloth", 34, Rarity.UNCOMMON, mage.cards.u.UnswervingSloth.class)); diff --git a/Mage/src/main/java/mage/abilities/keyword/ExhaustAbility.java b/Mage/src/main/java/mage/abilities/keyword/ExhaustAbility.java index 7528788e1b1..f8cd4e3f532 100644 --- a/Mage/src/main/java/mage/abilities/keyword/ExhaustAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/ExhaustAbility.java @@ -12,13 +12,27 @@ import mage.game.Game; */ public class ExhaustAbility extends ActivatedAbilityImpl { + private boolean withReminderText = true; + public ExhaustAbility(Effect effect, Cost cost) { super(Zone.BATTLEFIELD, effect, cost); } + public ExhaustAbility(Effect effect, Cost cost, boolean withReminderText) { + super(Zone.BATTLEFIELD, effect, cost); + this.setRuleVisible(false); + this.withReminderText = withReminderText; + } + private ExhaustAbility(final ExhaustAbility ability) { super(ability); this.maxActivationsPerGame = 1; + this.withReminderText = ability.withReminderText; + } + + public ExhaustAbility withReminderText(boolean withReminderText) { + this.withReminderText = withReminderText; + return this; } @Override @@ -42,6 +56,7 @@ public class ExhaustAbility extends ActivatedAbilityImpl { @Override public String getRule() { - return "Exhaust — " + super.getRule() + " (Activate each exhaust ability only once.)"; + return "Exhaust — " + super.getRule() + + (withReminderText ? " (Activate each exhaust ability only once.)" : ""); } } From 8b1b04cd7cf18a8cc6aa479a01c93ecc0805c08a Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sun, 16 Mar 2025 00:10:28 -0400 Subject: [PATCH 39/44] add assertion to ninjutsu test --- .../org/mage/test/cards/abilities/keywords/NinjutsuTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/NinjutsuTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/NinjutsuTest.java index 3b844e5a1bb..027689cada7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/NinjutsuTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/NinjutsuTest.java @@ -45,6 +45,7 @@ public class NinjutsuTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_COMBAT); execute(); + assertLife(playerB, 17); assertPowerToughness(playerA, shinobi, 3, 2); assertPermanentCount(playerA, "Illusion Token", 1); assertHandCount(playerA, drake, 1); From a6b3a20aeba12890968a38b37d91c8db02d2be36 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sun, 16 Mar 2025 00:16:49 -0400 Subject: [PATCH 40/44] fix Minion of the Wastes multiple replacement (related to f252525c) --- Mage.Sets/src/mage/cards/d/Dracoplasm.java | 1 + .../src/mage/cards/m/MinionOfTheWastes.java | 1 + Mage.Sets/src/mage/cards/n/NamelessRace.java | 1 + Mage.Sets/src/mage/cards/w/WoodElemental.java | 1 + .../single/tmp/MinionOfTheWastesTest.java | 22 +++++++++++++++++++ 5 files changed, 26 insertions(+) diff --git a/Mage.Sets/src/mage/cards/d/Dracoplasm.java b/Mage.Sets/src/mage/cards/d/Dracoplasm.java index 472827af344..a00e3178934 100644 --- a/Mage.Sets/src/mage/cards/d/Dracoplasm.java +++ b/Mage.Sets/src/mage/cards/d/Dracoplasm.java @@ -118,6 +118,7 @@ class DracoplasmEffect extends ReplacementEffectImpl { } ContinuousEffect effect = new SetBasePowerToughnessSourceEffect(power, toughness, Duration.WhileOnBattlefield); game.addEffect(effect, source); + this.discard(); // prevent multiple replacements e.g. on blink return false; } } diff --git a/Mage.Sets/src/mage/cards/m/MinionOfTheWastes.java b/Mage.Sets/src/mage/cards/m/MinionOfTheWastes.java index db1bb56c139..d0d0188faca 100644 --- a/Mage.Sets/src/mage/cards/m/MinionOfTheWastes.java +++ b/Mage.Sets/src/mage/cards/m/MinionOfTheWastes.java @@ -99,6 +99,7 @@ class MinionOfTheWastesEffect extends ReplacementEffectImpl { payAmount, payAmount, Duration.WhileOnBattlefield ), source); creature.addInfo("life paid", CardUtil.addToolTipMarkTags("Life paid: " + payAmount), game); + this.discard(); // prevent multiple replacements e.g. on blink return false; } } diff --git a/Mage.Sets/src/mage/cards/n/NamelessRace.java b/Mage.Sets/src/mage/cards/n/NamelessRace.java index 082a06b0516..8f92effeaf1 100644 --- a/Mage.Sets/src/mage/cards/n/NamelessRace.java +++ b/Mage.Sets/src/mage/cards/n/NamelessRace.java @@ -123,6 +123,7 @@ class NamelessRaceEffect extends ReplacementEffectImpl { payAmount, payAmount, Duration.WhileOnBattlefield ), source); creature.addInfo("life paid", CardUtil.addToolTipMarkTags("Life paid: " + payAmount), game); + this.discard(); // prevent multiple replacements e.g. on blink return false; } } diff --git a/Mage.Sets/src/mage/cards/w/WoodElemental.java b/Mage.Sets/src/mage/cards/w/WoodElemental.java index 4f2b29a430c..c415084aea7 100644 --- a/Mage.Sets/src/mage/cards/w/WoodElemental.java +++ b/Mage.Sets/src/mage/cards/w/WoodElemental.java @@ -107,6 +107,7 @@ class WoodElementalEffect extends ReplacementEffectImpl { } } game.addEffect(new SetBasePowerToughnessSourceEffect(value, value, Duration.WhileOnBattlefield), source); + this.discard(); // prevent multiple replacements e.g. on blink return false; } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MinionOfTheWastesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MinionOfTheWastesTest.java index 2d7e79a20f8..a75c1a76184 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MinionOfTheWastesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/MinionOfTheWastesTest.java @@ -32,4 +32,26 @@ public class MinionOfTheWastesTest extends CardTestPlayerBase { assertPowerToughness(playerA, minion, 3, 3); } + @Test + public void testFlicker() { + addCard(Zone.BATTLEFIELD, playerA, "Scrubland", 7); + addCard(Zone.HAND, playerA, minion); + addCard(Zone.HAND, playerA, "Cloudshift"); // flicker + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, minion); + setChoice(playerA, "X=3"); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Cloudshift", minion); + setChoice(playerA, "X=2"); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 15); + assertPowerToughness(playerA, minion, 2, 2); + } + + // Note similar cards: Wood Elemental, Nameless Race, Dracoplasm + } From 198fc02e0c07cf7998f6b148dc18559f0cf9a34d Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sun, 16 Mar 2025 00:20:24 -0400 Subject: [PATCH 41/44] Revert "fix #13307 (Volatile Stormdrake)" This reverts commit 37fc1737011156f9f431fd4c112c654a96d762ff. --- Mage.Sets/src/mage/cards/v/VolatileStormdrake.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Mage.Sets/src/mage/cards/v/VolatileStormdrake.java b/Mage.Sets/src/mage/cards/v/VolatileStormdrake.java index 52d1482ec65..b4a160e66f4 100644 --- a/Mage.Sets/src/mage/cards/v/VolatileStormdrake.java +++ b/Mage.Sets/src/mage/cards/v/VolatileStormdrake.java @@ -4,10 +4,11 @@ import mage.MageInt; import mage.MageObject; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.costs.Cost; import mage.abilities.costs.common.PayEnergyCost; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.SacrificeTargetEffect; import mage.abilities.effects.common.continuous.ExchangeControlTargetEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.HexproofBaseAbility; @@ -137,13 +138,10 @@ class VolatileStormdrakeEffect extends OneShotEffect { game.addEffect(effect, source); game.processAction(); controller.addCounters(CounterType.ENERGY.createInstance(4), controller.getId(), source, game); - Cost cost = new PayEnergyCost(targetPermanent.getManaValue()); - if (cost.canPay(source, source, controller.getId(), game) && - controller.chooseUse(Outcome.Benefit, "Pay " + cost.getText() + " to prevent sacrifice?", source, game) && - cost.pay(source, game, source, controller.getId(), false)) { - return true; - } - targetPermanent.sacrifice(source, game); + new DoIfCostPaid( + null, new SacrificeTargetEffect("", controller.getId()), + new PayEnergyCost(targetPermanent.getManaValue()), true + ).apply(game, source); return true; } } From fa88a3217d2fd50e119cda6f53ab83f43ad26a25 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sun, 16 Mar 2025 00:45:48 -0400 Subject: [PATCH 42/44] fix #13451 (Nether Traitor), add test --- Mage.Sets/src/mage/cards/n/NetherTraitor.java | 78 +++--------- .../cards/single/tsp/NetherTraitorTest.java | 118 ++++++++++++++++++ .../game/permanent/token/CarnivoreToken.java | 2 +- 3 files changed, 137 insertions(+), 61 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tsp/NetherTraitorTest.java diff --git a/Mage.Sets/src/mage/cards/n/NetherTraitor.java b/Mage.Sets/src/mage/cards/n/NetherTraitor.java index 3b4f04d605d..b14e3bb93be 100644 --- a/Mage.Sets/src/mage/cards/n/NetherTraitor.java +++ b/Mage.Sets/src/mage/cards/n/NetherTraitor.java @@ -1,11 +1,8 @@ - package mage.cards.n; -import java.util.UUID; import mage.MageInt; -import mage.MageObject; -import mage.abilities.TriggeredAbilityImpl; -import mage.abilities.costs.mana.ColoredManaCost; +import mage.abilities.common.DiesCreatureTriggeredAbility; +import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.DoIfCostPaid; import mage.abilities.effects.common.ReturnSourceFromGraveyardToBattlefieldEffect; import mage.abilities.keyword.HasteAbility; @@ -14,11 +11,12 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.ColoredManaSymbol; +import mage.constants.TargetController; import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.events.ZoneChangeEvent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.mageobject.AnotherPredicate; + +import java.util.UUID; /** * @@ -26,6 +24,12 @@ import mage.game.events.ZoneChangeEvent; */ public final class NetherTraitor extends CardImpl { + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(); + static { + filter.add(AnotherPredicate.instance); + filter.add(TargetController.YOU.getOwnerPredicate()); + } + public NetherTraitor(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.CREATURE},"{B}{B}"); this.subtype.add(SubType.SPIRIT); @@ -39,7 +43,11 @@ public final class NetherTraitor extends CardImpl { this.addAbility(ShadowAbility.getInstance()); // Whenever another creature is put into your graveyard from the battlefield, you may pay {B}. If you do, return Nether Traitor from your graveyard to the battlefield. - this.addAbility(new NetherTraitorTriggeredAbility()); + this.addAbility(new DiesCreatureTriggeredAbility(Zone.GRAVEYARD, new DoIfCostPaid( + new ReturnSourceFromGraveyardToBattlefieldEffect(), + new ManaCostsImpl<>("{B}") + ), false, filter, false + ).setTriggerPhrase("Whenever another creature is put into your graveyard from the battlefield, ")); } private NetherTraitor(final NetherTraitor card) { @@ -52,53 +60,3 @@ public final class NetherTraitor extends CardImpl { } } -class NetherTraitorTriggeredAbility extends TriggeredAbilityImpl { - - NetherTraitorTriggeredAbility(){ - super(Zone.GRAVEYARD, new DoIfCostPaid(new ReturnSourceFromGraveyardToBattlefieldEffect(), new ColoredManaCost(ColoredManaSymbol.B))); - setLeavesTheBattlefieldTrigger(true); - } - - private NetherTraitorTriggeredAbility(final NetherTraitorTriggeredAbility ability) { - super(ability); - } - - @Override - public NetherTraitorTriggeredAbility copy(){ - return new NetherTraitorTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.ZONE_CHANGE; - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - for (Zone z : Zone.values()) { - if (game.checkShortLivingLKI(sourceId, z) && z != Zone.GRAVEYARD) { - return false; - } - } - if (zEvent.isDiesEvent()) { - if (zEvent.getTarget() != null && - zEvent.getTarget().isOwnedBy(this.getControllerId()) && - zEvent.getTarget().isCreature(game)&& - !zEvent.getTarget().getId().equals(this.getSourceId())) { - return true; - } - } - return false; - } - - @Override - public String getRule() { - return "Whenever another creature is put into your graveyard from the battlefield, you may pay {B}. If you do, return {this} from your graveyard to the battlefield."; - } - - @Override - public boolean isInUseableZone(Game game, MageObject sourceObject, GameEvent event) { - return TriggeredAbilityImpl.isInUseableZoneDiesTrigger(this, sourceObject, event, game); - } -} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tsp/NetherTraitorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tsp/NetherTraitorTest.java new file mode 100644 index 00000000000..330b83237e2 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tsp/NetherTraitorTest.java @@ -0,0 +1,118 @@ +package org.mage.test.cards.single.tsp; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author xenohedron + */ +public class NetherTraitorTest extends CardTestPlayerBase { + + private static final String nt = "Nether Traitor"; + /* Haste; shadow + Whenever another creature is put into your graveyard from the battlefield, you may pay {B}. + If you do, return this card from your graveyard to the battlefield. + */ + private static final String gb = "Goblin Bombardment"; // Sacrifice a creature: 1 damage to any target + private static final String wg = "Warpath Ghoul"; // 3/2 + + @Test + public void testNetherTraitorBattlefieldGhoulDies() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, nt); + addCard(Zone.BATTLEFIELD, playerA, gb); + addCard(Zone.BATTLEFIELD, playerA, wg); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + setChoice(playerA, wg); // to sac + addTarget(playerA, playerB); // 1 damage + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 19); + assertGraveyardCount(playerA, wg, 1); + assertPermanentCount(playerA, nt, 1); + assertTapped("Swamp", false); + } + + @Test + public void testNetherTraitorBattlefieldAndDies() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, nt); + addCard(Zone.BATTLEFIELD, playerA, gb); + addCard(Zone.BATTLEFIELD, playerA, wg); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + setChoice(playerA, nt); // to sac + addTarget(playerA, playerB); // 1 damage + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 19); + assertGraveyardCount(playerA, nt, 1); + assertPermanentCount(playerA, wg, 1); + assertTapped("Swamp", false); + } + + @Test + public void testNetherTraitorGraveyardGhoulDies() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.GRAVEYARD, playerA, nt); + addCard(Zone.BATTLEFIELD, playerA, gb); + addCard(Zone.BATTLEFIELD, playerA, wg); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + setChoice(playerA, wg); // to sac + addTarget(playerA, playerB); // 1 damage + setChoice(playerA, true); // yes to pay B and return + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 19); + assertGraveyardCount(playerA, wg, 1); + assertPermanentCount(playerA, nt, 1); + assertTapped("Swamp", true); + } + + /* Ruling: + If Nether Traitor and another creature are put into your graveyard at the same time, + Nether Traitor's ability won't trigger. + This is because it must be in your graveyard before the creature dies in order for its ability + that returns it to the battlefield to trigger. + */ + + @Test + public void testNetherTraitorGhoulBothDie() { + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.BATTLEFIELD, playerA, nt); + addCard(Zone.BATTLEFIELD, playerA, "Tooth and Claw"); + // Sacrifice two creatures: Create a 3/1 red Beast creature token named Carnivore. + addCard(Zone.BATTLEFIELD, playerA, wg); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice two"); + setChoice(playerA, wg + "^" + nt); // to sac + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertPermanentCount(playerA, "Carnivore", 1); + assertGraveyardCount(playerA, wg, 1); + assertGraveyardCount(playerA, nt, 1); + assertTapped("Swamp", false); + } + +} diff --git a/Mage/src/main/java/mage/game/permanent/token/CarnivoreToken.java b/Mage/src/main/java/mage/game/permanent/token/CarnivoreToken.java index 2b5f57ae67f..cb0d03f0146 100644 --- a/Mage/src/main/java/mage/game/permanent/token/CarnivoreToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/CarnivoreToken.java @@ -10,7 +10,7 @@ import mage.constants.SubType; public final class CarnivoreToken extends TokenImpl { public CarnivoreToken() { - super("Carnivore Token", "3/1 red Beast creature token"); + super("Carnivore", "3/1 red Beast creature token"); cardType.add(CardType.CREATURE); color.setRed(true); subtype.add(SubType.BEAST); From c64a56acf7fb5552fc38340c04612a6abc40bd8c Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sun, 16 Mar 2025 01:09:31 -0400 Subject: [PATCH 43/44] add test for #13419 --- .../protection/gain/GainProtectionTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/protection/gain/GainProtectionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/protection/gain/GainProtectionTest.java index 6643a08bf56..f35a1f999cf 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/protection/gain/GainProtectionTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/protection/gain/GainProtectionTest.java @@ -135,4 +135,46 @@ public class GainProtectionTest extends CardTestPlayerBase { assertPermanentCount(playerB, "Pentarch Ward", 1); assertHandCount(playerB, 3); } + + // reported issue #13419 + @Test + public void testChoMannosBlessingContagion() { + String soltari = "Soltari Visionary"; + // Shadow (This creature can block or be blocked by only creatures with shadow.) + // Whenever this creature deals damage to a player, destroy target enchantment that player controls. + String contagion = "Contagion"; + // You may pay 1 life and exile a black card from your hand rather than pay this spell’s mana cost. + // Distribute two -2/-1 counters among one or two target creatures. + String cmb = "Cho-Manno's Blessing"; + // Flash + // Enchant creature + // As this Aura enters, choose a color. + // Enchanted creature has protection from the chosen color. This effect doesn’t remove this Aura. + + addCard(Zone.BATTLEFIELD, playerA, soltari); + addCard(Zone.HAND, playerB, contagion); + addCard(Zone.HAND, playerB, "Warpath Ghoul"); // black card + addCard(Zone.HAND, playerA, cmb); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, contagion, soltari); + setChoice(playerB, "Cast with alternative cost"); + setChoice(playerB, "Warpath Ghoul"); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, cmb, soltari, contagion); // in response + setChoice(playerA, "Black"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 19); + assertExileCount(playerB, "Warpath Ghoul", 1); + assertGraveyardCount(playerB, contagion, 1); + assertPermanentCount(playerA, soltari, 1); + assertPermanentCount(playerA, cmb, 1); + assertAttachedTo(playerA, cmb, soltari, true); + + } + } From 82d03b46e3bddc312770f652a587d1c4c69cca10 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sun, 16 Mar 2025 01:15:53 -0400 Subject: [PATCH 44/44] fix #13241 (Delney, Streetwise Lookout) --- Mage.Sets/src/mage/cards/d/DelneyStreetwiseLookout.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/d/DelneyStreetwiseLookout.java b/Mage.Sets/src/mage/cards/d/DelneyStreetwiseLookout.java index 6cf8e4aa6c7..0097635ec28 100644 --- a/Mage.Sets/src/mage/cards/d/DelneyStreetwiseLookout.java +++ b/Mage.Sets/src/mage/cards/d/DelneyStreetwiseLookout.java @@ -80,7 +80,7 @@ class DelneyStreetwiseLookoutEffect extends ReplacementEffectImpl { @Override public boolean applies(GameEvent event, Ability source, Game game) { - Permanent permanent = game.getPermanent(event.getSourceId()); + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId()); return permanent != null && permanent.isCreature(game) && permanent.isControlledBy(source.getControllerId())