From 676c2cb861609a0b8ca47281238d88843db2dad2 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:03:58 -0400 Subject: [PATCH 01/13] [MOC] Implement Deluxe Dragster (#10209) --- .../src/mage/cards/d/DeluxeDragster.java | 197 ++++++++++++++++++ .../mage/sets/MarchOfTheMachineCommander.java | 1 + 2 files changed, 198 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DeluxeDragster.java diff --git a/Mage.Sets/src/mage/cards/d/DeluxeDragster.java b/Mage.Sets/src/mage/cards/d/DeluxeDragster.java new file mode 100644 index 00000000000..5158109a7ab --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DeluxeDragster.java @@ -0,0 +1,197 @@ +package mage.cards.d; + +import mage.ApprovingObject; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleEvasionAbility; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.combat.CantBeBlockedByCreaturesSourceEffect; +import mage.abilities.keyword.CrewAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.card.OwnerIdPredicate; +import mage.game.Game; +import mage.game.events.DamagedPlayerEvent; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import mage.target.common.TargetCardInGraveyard; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DeluxeDragster extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent("except by Vehicles"); + + static { + filter.add(Predicates.not(SubType.VEHICLE.getPredicate())); + } + + public DeluxeDragster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{4}{U}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Deluxe Dragster can’t be blocked except by Vehicles. + this.addAbility(new SimpleEvasionAbility(new CantBeBlockedByCreaturesSourceEffect(filter, Duration.WhileOnBattlefield))); + + // Whenever Deluxe Dragster deals combat damage to a player, you may cast target instant or sorcery card from + // that player’s graveyard without paying its mana cost. If that spell would be put into a graveyard, exile it instead. + this.addAbility(new DeluxeDragsterTriggeredAbility()); + + // Crew 2 + this.addAbility(new CrewAbility(2)); + } + + private DeluxeDragster(final DeluxeDragster card) { + super(card); + } + + @Override + public DeluxeDragster copy() { + return new DeluxeDragster(this); + } +} + +class DeluxeDragsterTriggeredAbility extends TriggeredAbilityImpl { + + public DeluxeDragsterTriggeredAbility() { + super(Zone.BATTLEFIELD, new DeluxeDragsterEffect(), true); + } + + public DeluxeDragsterTriggeredAbility(final DeluxeDragsterTriggeredAbility ability) { + super(ability); + } + + @Override + public DeluxeDragsterTriggeredAbility copy() { + return new DeluxeDragsterTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DAMAGED_PLAYER; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!event.getSourceId().equals(this.sourceId) || !((DamagedPlayerEvent) event).isCombatDamage()) { + return false; + } + Player damagedPlayer = game.getPlayer(event.getTargetId()); + if (damagedPlayer == null) { + return false; + } + FilterCard filter = new FilterCard("instant or sorcery in " + damagedPlayer.getName() + "'s graveyard"); + filter.add(Predicates.or(CardType.INSTANT.getPredicate(), CardType.SORCERY.getPredicate())); + filter.add(new OwnerIdPredicate(damagedPlayer.getId())); + TargetCardInGraveyard target = new TargetCardInGraveyard(filter); + this.getTargets().clear(); + this.addTarget(target); + return true; + } + + @Override + public String getRule() { + return "Whenever {this} deals combat damage to a player, " + + "you may cast target instant or sorcery card from " + + "that player's graveyard without paying its mana " + + "cost. If that spell would be put into a graveyard, " + + "exile it instead."; + } +} + +class DeluxeDragsterEffect extends OneShotEffect { + + public DeluxeDragsterEffect() { + super(Outcome.PlayForFree); + this.staticText = "you may cast target instant or sorcery card from " + + "that player's graveyard without paying its mana " + + "cost. If that spell would be put into a graveyard, " + + "exile it instead"; + } + + public DeluxeDragsterEffect(final DeluxeDragsterEffect effect) { + super(effect); + } + + @Override + public DeluxeDragsterEffect copy() { + return new DeluxeDragsterEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + Card targetCard = game.getCard(source.getFirstTarget()); + if (targetCard != null) { + game.getState().setValue("PlayFromNotOwnHandZone" + targetCard.getId(), Boolean.TRUE); + Boolean cardWasCast = controller.cast(controller.chooseAbilityForCast(targetCard, game, true), + game, true, new ApprovingObject(source, game)); + game.getState().setValue("PlayFromNotOwnHandZone" + targetCard.getId(), null); + if (cardWasCast) { + ContinuousEffect effect = new DeluxeDragsterReplacementEffect(); + effect.setTargetPointer(new FixedTarget(targetCard.getId(), game.getState().getZoneChangeCounter(targetCard.getId()))); + game.addEffect(effect, source); + } + } + return true; + } + return false; + } +} + +class DeluxeDragsterReplacementEffect extends ReplacementEffectImpl { + + public DeluxeDragsterReplacementEffect() { + super(Duration.EndOfTurn, Outcome.Exile); + staticText = "If that spell would be put into a graveyard this turn, exile it instead"; + } + + public DeluxeDragsterReplacementEffect(final DeluxeDragsterReplacementEffect effect) { + super(effect); + } + + @Override + public DeluxeDragsterReplacementEffect copy() { + return new DeluxeDragsterReplacementEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + ((ZoneChangeEvent) event).setToZone(Zone.EXILED); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + ZoneChangeEvent zEvent = (ZoneChangeEvent) event; + return zEvent.getToZone() == Zone.GRAVEYARD + && event.getTargetId().equals(getTargetPointer().getFirst(game, source)); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java index 962e2cd2294..155d3f08945 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java @@ -80,6 +80,7 @@ public final class MarchOfTheMachineCommander extends ExpansionSet { cards.add(new SetCardInfo("Cultivator's Caravan", 354, Rarity.RARE, mage.cards.c.CultivatorsCaravan.class)); cards.add(new SetCardInfo("Curse of Opulence", 274, Rarity.UNCOMMON, mage.cards.c.CurseOfOpulence.class)); cards.add(new SetCardInfo("Death-Greeter's Champion", 30, Rarity.RARE, mage.cards.d.DeathGreetersChampion.class)); + cards.add(new SetCardInfo("Deluxe Dragster", 21, Rarity.RARE, mage.cards.d.DeluxeDragster.class)); cards.add(new SetCardInfo("Despark", 322, Rarity.UNCOMMON, mage.cards.d.Despark.class)); cards.add(new SetCardInfo("Devouring Light", 180, Rarity.UNCOMMON, mage.cards.d.DevouringLight.class)); cards.add(new SetCardInfo("Distant Melody", 220, Rarity.COMMON, mage.cards.d.DistantMelody.class)); From 9737526f17cc3aa944ce67395118e1e802f43359 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 11 Apr 2023 18:54:18 -0400 Subject: [PATCH 02/13] fix verify failure --- .../src/mage/cards/r/RashmiAndRagavan.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Mage.Sets/src/mage/cards/r/RashmiAndRagavan.java b/Mage.Sets/src/mage/cards/r/RashmiAndRagavan.java index 6470177fba6..4a7a30e35b7 100644 --- a/Mage.Sets/src/mage/cards/r/RashmiAndRagavan.java +++ b/Mage.Sets/src/mage/cards/r/RashmiAndRagavan.java @@ -45,10 +45,10 @@ public final class RashmiAndRagavan extends CardImpl { this.toughness = new MageInt(4); // Whenever you cast your first spell during each of your turns, - // exile the top card of target opponent’s library and create a Treasure token. - // Then you may cast the exiled card without paying its mana cost if it’s a spell with mana value + // exile the top card of target opponent's library and create a Treasure token. + // Then you may cast the exiled card without paying its mana cost if it's a spell with mana value // less than the number of artifacts you control. - // If you don’t cast it this way, you may cast it this turn. + // If you don't cast it this way, you may cast it this turn. Ability ability = new RashmiAndRagavanTriggeredAbility(); ability.addTarget(new TargetOpponent()); this.addAbility(ability, new SpellsCastWatcher()); @@ -98,9 +98,9 @@ class RashmiAndRagavanTriggeredAbility extends SpellCastControllerTriggeredAbili @Override public String getRule() { return "Whenever you cast your first spell during each of your turns, exile the top card of target " - + "opponent’s library and create a Treasure token. Then you may cast the exiled card without " - + "paying its mana cost if it’s a spell with mana value less than the number of artifacts you " - + "control. If you don’t cast it this way, you may cast it this turn."; + + "opponent's library and create a Treasure token. Then you may cast the exiled card without " + + "paying its mana cost if it's a spell with mana value less than the number of artifacts you " + + "control. If you don't cast it this way, you may cast it this turn."; } } @@ -108,9 +108,9 @@ class RashmiAndRagavanEffect extends OneShotEffect { RashmiAndRagavanEffect() { super(Outcome.PlayForFree); - this.staticText = "exile the top card of target opponent’s library and create a Treasure token. " - + "Then you may cast the exiled card without paying its mana cost if it’s a spell with mana value " - + "less than the number of artifacts you control. If you don’t cast it this way, " + this.staticText = "exile the top card of target opponent's library and create a Treasure token. " + + "Then you may cast the exiled card without paying its mana cost if it's a spell with mana value " + + "less than the number of artifacts you control. If you don't cast it this way, " + "you may cast it this turn"; } From 8d48429f5fbb574dde3fb471c2c6dc647655e037 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 11 Apr 2023 19:51:03 -0400 Subject: [PATCH 03/13] [MOC] Implement Dance with Calamity --- .../src/mage/cards/d/DanceWithCalamity.java | 90 +++++++++++++++++++ .../mage/sets/MarchOfTheMachineCommander.java | 1 + 2 files changed, 91 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DanceWithCalamity.java diff --git a/Mage.Sets/src/mage/cards/d/DanceWithCalamity.java b/Mage.Sets/src/mage/cards/d/DanceWithCalamity.java new file mode 100644 index 00000000000..5d947b3f73a --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DanceWithCalamity.java @@ -0,0 +1,90 @@ +package mage.cards.d; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.players.Player; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DanceWithCalamity extends CardImpl { + + public DanceWithCalamity(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{7}{R}"); + + // Shuffle your library. As many times as you choose, you may exile the top card of your library. If the total mana value of the cards exiled this way is 13 or less, you may cast any number of spells from among those cards without paying their mana costs. + this.getSpellAbility().addEffect(new DanceWithCalamityEffect()); + } + + private DanceWithCalamity(final DanceWithCalamity card) { + super(card); + } + + @Override + public DanceWithCalamity copy() { + return new DanceWithCalamity(this); + } +} + +class DanceWithCalamityEffect extends OneShotEffect { + + DanceWithCalamityEffect() { + super(Outcome.Benefit); + staticText = "shuffle your library. As many times as you choose, you may exile the top card of your library. " + + "If the total mana value of the cards exiled this way is 13 or less, you may cast any number " + + "of spells from among those cards without paying their mana costs"; + } + + private DanceWithCalamityEffect(final DanceWithCalamityEffect effect) { + super(effect); + } + + @Override + public DanceWithCalamityEffect copy() { + return new DanceWithCalamityEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(); + while (player.getLibrary().hasCards()) { + int totalMV = cards + .getCards(game) + .stream() + .mapToInt(MageObject::getManaValue) + .sum(); + if (!player.chooseUse( + outcome, "Exile the top card of your library?", + "Current total mana value is " + totalMV, + "Yes", "No", source, game + )) { + break; + } + Card card = player.getLibrary().getFromTop(game); + player.moveCards(card, Zone.EXILED, source, game); + cards.add(card); + } + if (cards + .getCards(game) + .stream() + .mapToInt(MageObject::getManaValue) + .sum() <= 13) { + CardUtil.castMultipleWithAttributeForFree(player, source, game, cards, StaticFilters.FILTER_CARD); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java index 155d3f08945..589396b1b85 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java @@ -79,6 +79,7 @@ public final class MarchOfTheMachineCommander extends ExpansionSet { cards.add(new SetCardInfo("Cultivate", 295, Rarity.COMMON, mage.cards.c.Cultivate.class)); cards.add(new SetCardInfo("Cultivator's Caravan", 354, Rarity.RARE, mage.cards.c.CultivatorsCaravan.class)); cards.add(new SetCardInfo("Curse of Opulence", 274, Rarity.UNCOMMON, mage.cards.c.CurseOfOpulence.class)); + cards.add(new SetCardInfo("Dance with Calamity", 29, Rarity.RARE, mage.cards.d.DanceWithCalamity.class)); cards.add(new SetCardInfo("Death-Greeter's Champion", 30, Rarity.RARE, mage.cards.d.DeathGreetersChampion.class)); cards.add(new SetCardInfo("Deluxe Dragster", 21, Rarity.RARE, mage.cards.d.DeluxeDragster.class)); cards.add(new SetCardInfo("Despark", 322, Rarity.UNCOMMON, mage.cards.d.Despark.class)); From 988626352dfe5ccf9abee99415d16b2ab0061729 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 11 Apr 2023 19:56:28 -0400 Subject: [PATCH 04/13] [MOC] Implement Hedron Detonator --- .../src/mage/cards/h/HedronDetonator.java | 59 +++++++++++++++++++ .../mage/sets/MarchOfTheMachineCommander.java | 1 + 2 files changed, 60 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HedronDetonator.java diff --git a/Mage.Sets/src/mage/cards/h/HedronDetonator.java b/Mage.Sets/src/mage/cards/h/HedronDetonator.java new file mode 100644 index 00000000000..0c0a170531a --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HedronDetonator.java @@ -0,0 +1,59 @@ +package mage.cards.h; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.common.SacrificeTargetCost; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.effects.common.ExileTopXMayPlayUntilEndOfTurnEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledArtifactPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.target.common.TargetControlledPermanent; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HedronDetonator extends CardImpl { + + private static final FilterControlledPermanent filter = new FilterControlledArtifactPermanent("artifacts"); + + public HedronDetonator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{R}"); + + this.subtype.add(SubType.GOBLIN); + this.subtype.add(SubType.ARTIFICER); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Whenever an artifact enters the battlefield under your control, Hedron Detonator deals 1 damage to target opponent. + Ability ability = new EntersBattlefieldControlledTriggeredAbility( + new DamageTargetEffect(1), StaticFilters.FILTER_PERMANENT_ARTIFACT + ); + ability.addTarget(new TargetOpponent()); + this.addAbility(ability); + + // {T}, Sacrifice two artifacts: Exile the top card of your library. You may play that card this turn. + ability = new SimpleActivatedAbility(new ExileTopXMayPlayUntilEndOfTurnEffect(1), new TapSourceCost()); + ability.addCost(new SacrificeTargetCost(new TargetControlledPermanent(2, filter))); + this.addAbility(ability); + } + + private HedronDetonator(final HedronDetonator card) { + super(card); + } + + @Override + public HedronDetonator copy() { + return new HedronDetonator(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java index 589396b1b85..798e666b8d3 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java @@ -139,6 +139,7 @@ public final class MarchOfTheMachineCommander extends ExpansionSet { cards.add(new SetCardInfo("Hamza, Guardian of Arashin", 327, Rarity.UNCOMMON, mage.cards.h.HamzaGuardianOfArashin.class)); cards.add(new SetCardInfo("Heaven // Earth", 328, Rarity.RARE, mage.cards.h.HeavenEarth.class)); cards.add(new SetCardInfo("Hedron Archive", 359, Rarity.UNCOMMON, mage.cards.h.HedronArchive.class)); + cards.add(new SetCardInfo("Hedron Detonator", 31, Rarity.RARE, mage.cards.h.HedronDetonator.class)); cards.add(new SetCardInfo("Hellkite Igniter", 284, Rarity.RARE, mage.cards.h.HellkiteIgniter.class)); cards.add(new SetCardInfo("Herald of Hoofbeats", 22, Rarity.RARE, mage.cards.h.HeraldOfHoofbeats.class)); cards.add(new SetCardInfo("Herald's Horn", 360, Rarity.UNCOMMON, mage.cards.h.HeraldsHorn.class)); From 8c1eaed97440d4194e2dfee719b9ad547c4b7d95 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 11 Apr 2023 20:06:07 -0400 Subject: [PATCH 05/13] [MOC] Implement Emergent Woodwurm --- .../src/mage/cards/e/EmergentWoodwurm.java | 81 +++++++++++++++++++ .../mage/sets/MarchOfTheMachineCommander.java | 1 + 2 files changed, 82 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/e/EmergentWoodwurm.java diff --git a/Mage.Sets/src/mage/cards/e/EmergentWoodwurm.java b/Mage.Sets/src/mage/cards/e/EmergentWoodwurm.java new file mode 100644 index 00000000000..9f3893f3e3c --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EmergentWoodwurm.java @@ -0,0 +1,81 @@ +package mage.cards.e; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.SourcePermanentPowerCount; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.abilities.keyword.BackupAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.PutCards; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.common.FilterPermanentCard; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EmergentWoodwurm extends CardImpl { + + private static final FilterCard filter = new FilterPermanentCard("permanent card with mana value X or less"); + + static { + filter.add(EmergentWoodwurmPredicate.instance); + } + + private static final DynamicValue xValue = new SourcePermanentPowerCount(false); + + public EmergentWoodwurm(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{6}{G}"); + + this.subtype.add(SubType.WURM); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Backup 3 + BackupAbility backupAbility = new BackupAbility(this, 3); + this.addAbility(backupAbility); + + // Whenever this creature attacks, look at the top X cards of your library, where X is its power. You may put a permanent card with mana value X or less from among them onto the battlefield. Put the rest on the bottom of your library in a random order. + backupAbility.addAbility(new AttacksTriggeredAbility(new LookLibraryAndPickControllerEffect( + xValue, 1, filter, PutCards.BATTLEFIELD, PutCards.BOTTOM_RANDOM + )).setTriggerPhrase("Whenever this creature attacks, ")); + } + + private EmergentWoodwurm(final EmergentWoodwurm card) { + super(card); + } + + @Override + public EmergentWoodwurm copy() { + return new EmergentWoodwurm(this); + } +} + +enum EmergentWoodwurmPredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return Optional + .of(input) + .map(ObjectSourcePlayer::getSource) + .map(ability -> ability.getSourcePermanentOrLKI(game)) + .filter(Objects::nonNull) + .map(MageObject::getPower) + .map(MageInt::getValue) + .map(i -> input.getObject().getManaValue() <= i) + .orElse(false); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java index 798e666b8d3..80c0c3c3a84 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java @@ -91,6 +91,7 @@ public final class MarchOfTheMachineCommander extends ExpansionSet { cards.add(new SetCardInfo("Echo Storm", 221, Rarity.RARE, mage.cards.e.EchoStorm.class)); cards.add(new SetCardInfo("Elite Scaleguard", 181, Rarity.UNCOMMON, mage.cards.e.EliteScaleguard.class)); cards.add(new SetCardInfo("Elspeth, Sun's Champion", 182, Rarity.MYTHIC, mage.cards.e.ElspethSunsChampion.class)); + cards.add(new SetCardInfo("Emergent Woodwurm", 37, Rarity.RARE, mage.cards.e.EmergentWoodwurm.class)); cards.add(new SetCardInfo("Emeria Angel", 183, Rarity.RARE, mage.cards.e.EmeriaAngel.class)); cards.add(new SetCardInfo("Enduring Scalelord", 325, Rarity.UNCOMMON, mage.cards.e.EnduringScalelord.class)); cards.add(new SetCardInfo("Ephemeral Shields", 184, Rarity.COMMON, mage.cards.e.EphemeralShields.class)); From 77382152e76ca8ab3ea324f12ee37831544c469c Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 11 Apr 2023 20:14:20 -0400 Subject: [PATCH 06/13] [USG] rework Veiled Sentry --- Mage.Sets/src/mage/cards/v/VeiledSentry.java | 51 +++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/Mage.Sets/src/mage/cards/v/VeiledSentry.java b/Mage.Sets/src/mage/cards/v/VeiledSentry.java index bef85b0c6c0..33e034f9a5b 100644 --- a/Mage.Sets/src/mage/cards/v/VeiledSentry.java +++ b/Mage.Sets/src/mage/cards/v/VeiledSentry.java @@ -1,15 +1,14 @@ package mage.cards.v; import mage.abilities.Ability; -import mage.abilities.TriggeredAbility; import mage.abilities.common.SpellCastOpponentTriggeredAbility; +import mage.abilities.condition.Condition; import mage.abilities.condition.common.SourceMatchesFilterCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; import mage.abilities.effects.ContinuousEffectImpl; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; -import mage.filter.FilterSpell; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; @@ -22,14 +21,17 @@ import java.util.UUID; */ public final class VeiledSentry extends CardImpl { + private static final Condition condition = new SourceMatchesFilterCondition(StaticFilters.FILTER_PERMANENT_ENCHANTMENT); + public VeiledSentry(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}"); // When an opponent casts a spell, if Veiled Sentry is an enchantment, Veiled Sentry becomes an Illusion creature with power and toughness each equal to that spell's converted mana cost. - TriggeredAbility ability = new SpellCastOpponentTriggeredAbility(Zone.BATTLEFIELD, new VeiledSentryEffect(), new FilterSpell(), false, SetTargetPointer.SPELL); - this.addAbility(new ConditionalInterveningIfTriggeredAbility(ability, new SourceMatchesFilterCondition(StaticFilters.FILTER_PERMANENT_ENCHANTMENT), - "Whenever an opponent casts a spell, if Veiled Sentry is an enchantment, Veil Sentry becomes an Illusion creature with power and toughness equal to that spell's mana value.")); - + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new SpellCastOpponentTriggeredAbility(new VeiledSentryEffect(), false), + condition, "Whenever an opponent casts a spell, if {this} is an enchantment, " + + "{this} becomes an Illusion creature with power and toughness equal to that spell's mana value." + )); } private VeiledSentry(final VeiledSentry card) { @@ -44,6 +46,8 @@ public final class VeiledSentry extends CardImpl { class VeiledSentryEffect extends ContinuousEffectImpl { + private int spellMV = 0; + public VeiledSentryEffect() { super(Duration.Custom, Outcome.BecomeCreature); staticText = "{this} becomes an Illusion creature with power and toughness equal to that spell's mana value"; @@ -59,30 +63,31 @@ class VeiledSentryEffect extends ContinuousEffectImpl { } @Override - public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { - Permanent veiledSentry = game.getPermanent(source.getSourceId()); - Spell spellCast = game.getSpell(targetPointer.getFirst(game, source)); - if (spellCast != null) { - game.getState().setValue(source + "cmcSpell", spellCast.getManaValue()); + public void init(Ability source, Game game) { + Spell spell = (Spell) getValue("spellCast"); + if (spell != null) { + spellMV = spell.getManaValue(); } - if (veiledSentry == null) { + } + + @Override + public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + discard(); return false; } switch (layer) { case TypeChangingEffects_4: - veiledSentry.removeAllCardTypes(game); - veiledSentry.removeAllSubTypes(game); - veiledSentry.addCardType(game, CardType.CREATURE); - veiledSentry.addSubType(game, SubType.ILLUSION); + permanent.removeAllCardTypes(game); + permanent.removeAllSubTypes(game); + permanent.addCardType(game, CardType.CREATURE); + permanent.addSubType(game, SubType.ILLUSION); break; - case PTChangingEffects_7: - if (game.getState().getValue(source + "cmcSpell") != null) { - int cmc = (int) game.getState().getValue(source + "cmcSpell"); - if (sublayer == SubLayer.SetPT_7b) { - veiledSentry.addPower(cmc); - veiledSentry.addToughness(cmc); - } + if (sublayer == SubLayer.SetPT_7b) { + permanent.getPower().setModifiedBaseValue(spellMV); + permanent.getToughness().setModifiedBaseValue(spellMV); } } return true; From e9a16606dca3a48e9e7c785bad63b5772c014668 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 11 Apr 2023 22:16:20 -0400 Subject: [PATCH 07/13] [UGL] small additional fix to Veiled Sentry --- Mage.Sets/src/mage/cards/v/VeiledSentry.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage.Sets/src/mage/cards/v/VeiledSentry.java b/Mage.Sets/src/mage/cards/v/VeiledSentry.java index 33e034f9a5b..c9aaf0e0913 100644 --- a/Mage.Sets/src/mage/cards/v/VeiledSentry.java +++ b/Mage.Sets/src/mage/cards/v/VeiledSentry.java @@ -55,6 +55,7 @@ class VeiledSentryEffect extends ContinuousEffectImpl { public VeiledSentryEffect(final VeiledSentryEffect effect) { super(effect); + this.spellMV = effect.spellMV; } @Override @@ -64,6 +65,7 @@ class VeiledSentryEffect extends ContinuousEffectImpl { @Override public void init(Ability source, Game game) { + super.init(source, game); Spell spell = (Spell) getValue("spellCast"); if (spell != null) { spellMV = spell.getManaValue(); From 47ee0f7f5a9516b1550d1820f05705a02307e3e3 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 12 Apr 2023 09:48:28 +0400 Subject: [PATCH 08/13] Voice Of Resurgence - fixed token --- .../java/mage/game/permanent/token/VoiceOfResurgenceToken.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage/src/main/java/mage/game/permanent/token/VoiceOfResurgenceToken.java b/Mage/src/main/java/mage/game/permanent/token/VoiceOfResurgenceToken.java index 0a8a092e13c..35b302096b4 100644 --- a/Mage/src/main/java/mage/game/permanent/token/VoiceOfResurgenceToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/VoiceOfResurgenceToken.java @@ -17,7 +17,7 @@ import java.util.Arrays; public final class VoiceOfResurgenceToken extends TokenImpl { public VoiceOfResurgenceToken() { - super("Elemental Token", "X/X green and white Elemental creature with with \"This creature's power and toughness are each equal to the number of creatures you control."); + super("Elemental Token", "green and white Elemental creature token with \"This creature's power and toughness are each equal to the number of creatures you control."); setOriginalExpansionSetCode("DGM"); cardType.add(CardType.CREATURE); color.setGreen(true); From 5bff03f57a7bf5ed3184e0d3cae2e6a2bfc2d9d7 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 12 Apr 2023 09:51:19 +0400 Subject: [PATCH 09/13] Tests: improved verify tests for tokens data, added how-to fix hints, added new checks (disabled by default) --- .../java/mage/verify/VerifyCardDataTest.java | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index e1cf15cd944..bf588b24f42 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -1283,6 +1283,8 @@ public class VerifyCardDataTest { if (token == null) { errorsList.add("Error: token must have default constructor with zero params: " + tokenClass.getName()); } else if (tokDataNamesIndex.getOrDefault(token.getName().replace(" Token", ""), "").isEmpty()) { + // how-to fix: public token must be downloadable, so tok-data must contain miss set + // (also don't forget to add new set to scryfall download) errorsList.add("Error: can't find data in card-pictures-tok.txt for token: " + tokenClass.getName() + " -> " + token.getName()); } } @@ -1312,20 +1314,45 @@ public class VerifyCardDataTest { } // set uses tokens, but tok data miss it setsWithTokens.forEach((setCode, sourceCards) -> { - if (!tokDataTokensBySetIndex.containsKey(setCode)) { + List setTokens = tokDataTokensBySetIndex.getOrDefault(setCode, null); + if (setTokens == null) { // it's not a problem -- just find set's cards without real tokens for image tests - // (most use cases: promo sets) - warningsList.add("info, set has cards with token abilities, but it haven't token data: " + // Possible reasons: + // - promo sets with cards without tokens (nothing to do with it) + // - miss set from tok-data (must add new set to tok-data and scryfall download) + warningsList.add("info, set's cards uses tokens but tok-data haven't it: " + setCode + " - " + sourceCards.stream().map(MageObject::getName).collect(Collectors.joining(", "))); + } else { + // Card can be checked on scryfall like "set:set_code oracle:token_name oracle:token" + // Possible reasons for un-used tokens: + // - normal use case: tok-data contains wrong token data and must be removed + // - normal use case: card uses wrong rules text (must contain "create" and "token" words) + // - rare use case: un-implemented card that uses a token + setTokens.forEach(token -> { + if (token.getName().contains("Plane - ")) { + // cards don't put it to the game, so no related cards + return; + } + String needTokenName = token.getName() + .replace(" Token", "") + .replace("Emblem ", ""); + // need add card name, so it will skip no name emblems like Sarkhan, the Dragonspeaker + if (sourceCards.stream() + .map(card -> card.getName() + " - " + String.join(", ", card.getRules())) + .noneMatch(s -> s.contains(needTokenName))) { + warningsList.add("info, tok-data has un-used tokens: " + + token.getSet() + " - " + token.getName()); + } + }); } }); // tok data have tokens, but cards from set are miss tokDataTokensBySetIndex.forEach((setCode, setTokens) -> { if (!setsWithTokens.containsKey(setCode)) { - // possible reasons: - // - bad: outdated set code in tokens database (must use exists set codes, see additional check with token's codes) - // - ok: promo set contains additional tokens for main set -- it's ok and must be ignored (example: Saproling in E02) - warningsList.add("warning, tok data has set with tokens, but real set haven't cards with it: " + // Possible reasons: + // - outdated set code in tokens database (must be fixed by new set code, another verify check it) + // - promo set contains additional tokens for main set (it's ok and must be ignored, example: Saproling in E02) + warningsList.add("warning, tok-data has tokens, but real set haven't cards with it: " + setCode + " - " + setTokens.stream().map(CardDownloadData::getName).collect(Collectors.joining(", "))); } }); From 0ea5ada993dddb14a2c007eda0a211d35393a6b2 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 12 Apr 2023 08:15:43 -0400 Subject: [PATCH 10/13] [MOC] Implement Darksteel Splicer --- .../src/mage/cards/d/DarksteelSplicer.java | 62 +++++++++++++++++++ .../mage/sets/MarchOfTheMachineCommander.java | 1 + 2 files changed, 63 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DarksteelSplicer.java diff --git a/Mage.Sets/src/mage/cards/d/DarksteelSplicer.java b/Mage.Sets/src/mage/cards/d/DarksteelSplicer.java new file mode 100644 index 00000000000..a1a98ed45ae --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DarksteelSplicer.java @@ -0,0 +1,62 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldThisOrAnotherTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.OpponentsCount; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +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.mageobject.AnotherPredicate; +import mage.game.permanent.token.PhyrexianGolemToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DarksteelSplicer extends CardImpl { + + private static final FilterPermanent filter = new FilterPermanent(SubType.PHYREXIAN, "nontoken Phyrexian"); + + static { + filter.add(AnotherPredicate.instance); + } + + private static final FilterPermanent filter2 = new FilterPermanent(SubType.GOLEM, "Golems"); + + public DarksteelSplicer(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{6}{W}"); + + this.subtype.add(SubType.PHYREXIAN); + this.subtype.add(SubType.ARTIFICER); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Whenever Darksteel Splicer or another nontoken Phyrexian enters the battlefield under your control, create X 3/3 colorless Phyrexian Golem artifact creature tokens, where X is the number of opponents you have. + this.addAbility(new EntersBattlefieldThisOrAnotherTriggeredAbility( + new CreateTokenEffect(new PhyrexianGolemToken(), OpponentsCount.instance), + filter, false, true + )); + + // Golems you control have indestructible. + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect( + IndestructibleAbility.getInstance(), Duration.WhileOnBattlefield, filter2 + ))); + } + + private DarksteelSplicer(final DarksteelSplicer card) { + super(card); + } + + @Override + public DarksteelSplicer copy() { + return new DarksteelSplicer(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java index 80c0c3c3a84..0dd316fb1a8 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java @@ -80,6 +80,7 @@ public final class MarchOfTheMachineCommander extends ExpansionSet { cards.add(new SetCardInfo("Cultivator's Caravan", 354, Rarity.RARE, mage.cards.c.CultivatorsCaravan.class)); cards.add(new SetCardInfo("Curse of Opulence", 274, Rarity.UNCOMMON, mage.cards.c.CurseOfOpulence.class)); cards.add(new SetCardInfo("Dance with Calamity", 29, Rarity.RARE, mage.cards.d.DanceWithCalamity.class)); + cards.add(new SetCardInfo("Darksteel Splicer", 13, Rarity.RARE, mage.cards.d.DarksteelSplicer.class)); cards.add(new SetCardInfo("Death-Greeter's Champion", 30, Rarity.RARE, mage.cards.d.DeathGreetersChampion.class)); cards.add(new SetCardInfo("Deluxe Dragster", 21, Rarity.RARE, mage.cards.d.DeluxeDragster.class)); cards.add(new SetCardInfo("Despark", 322, Rarity.UNCOMMON, mage.cards.d.Despark.class)); From 771c291d789aaf2b3868f2ee9fb153e221edc05e Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 12 Apr 2023 08:30:34 -0400 Subject: [PATCH 11/13] [MOC] Implement Conjurer's Mantle --- .../src/mage/cards/c/ConjurersMantle.java | 80 +++++++++++++++++++ .../mage/sets/MarchOfTheMachineCommander.java | 1 + 2 files changed, 81 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/ConjurersMantle.java diff --git a/Mage.Sets/src/mage/cards/c/ConjurersMantle.java b/Mage.Sets/src/mage/cards/c/ConjurersMantle.java new file mode 100644 index 00000000000..3af905208df --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ConjurersMantle.java @@ -0,0 +1,80 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.AttacksAttachedTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; +import mage.abilities.effects.common.continuous.BoostEquippedEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.EquipAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; + +import java.util.Objects; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ConjurersMantle extends CardImpl { + + private static final FilterCard filter + = new FilterCard("a card that shares a creature type with that creature"); + + static { + filter.add(ConjurersMantlePredicate.instance); + } + + public ConjurersMantle(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{1}{W}"); + + this.subtype.add(SubType.EQUIPMENT); + + // Equipped creature gets +1/+1 and has vigilance. + Ability ability = new SimpleStaticAbility(new BoostEquippedEffect(1, 1)); + ability.addEffect(new GainAbilityAttachedEffect( + VigilanceAbility.getInstance(), AttachmentType.EQUIPMENT + ).setText("and has vigilance")); + this.addAbility(ability); + + // Whenever equipped creature attacks, look at the top six cards of your library. You may reveal a card that shares a creature type with that creature from among them and put it into your hand. Put the rest on the bottom of your library in a random order. + this.addAbility(new AttacksAttachedTriggeredAbility(new LookLibraryAndPickControllerEffect( + 6, 1, filter, PutCards.HAND, PutCards.BOTTOM_RANDOM + ), AttachmentType.EQUIPMENT, false, SetTargetPointer.PERMANENT)); + + // Equip {1} + this.addAbility(new EquipAbility(1, false)); + } + + private ConjurersMantle(final ConjurersMantle card) { + super(card); + } + + @Override + public ConjurersMantle copy() { + return new ConjurersMantle(this); + } +} + +enum ConjurersMantlePredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return input + .getSource() + .getEffects() + .stream() + .map(effect -> effect.getTargetPointer().getFirst(game, input.getSource())) + .map(game::getPermanent) + .filter(Objects::nonNull) + .anyMatch(permanent -> permanent.shareCreatureTypes(game, input.getObject())); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java index 0dd316fb1a8..1f2e3ce69e1 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java @@ -72,6 +72,7 @@ public final class MarchOfTheMachineCommander extends ExpansionSet { cards.add(new SetCardInfo("Commander's Sphere", 352, Rarity.COMMON, mage.cards.c.CommandersSphere.class)); cards.add(new SetCardInfo("Conclave Mentor", 320, Rarity.UNCOMMON, mage.cards.c.ConclaveMentor.class)); cards.add(new SetCardInfo("Conclave Tribunal", 178, Rarity.UNCOMMON, mage.cards.c.ConclaveTribunal.class)); + cards.add(new SetCardInfo("Conjurer's Mantle", 12, Rarity.RARE, mage.cards.c.ConjurersMantle.class)); cards.add(new SetCardInfo("Constable of the Realm", 179, Rarity.UNCOMMON, mage.cards.c.ConstableOfTheRealm.class)); cards.add(new SetCardInfo("Corpse Knight", 321, Rarity.UNCOMMON, mage.cards.c.CorpseKnight.class)); cards.add(new SetCardInfo("Coveted Jewel", 353, Rarity.RARE, mage.cards.c.CovetedJewel.class)); From 203defa56dd217df7747a8a8cd85ac5f3ff82fe0 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 12 Apr 2023 08:49:45 -0400 Subject: [PATCH 12/13] [MOC] Implement Mirror-Style Master --- .../src/mage/cards/m/MirrorStyleMaster.java | 104 ++++++++++++++++++ .../mage/sets/MarchOfTheMachineCommander.java | 1 + 2 files changed, 105 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MirrorStyleMaster.java diff --git a/Mage.Sets/src/mage/cards/m/MirrorStyleMaster.java b/Mage.Sets/src/mage/cards/m/MirrorStyleMaster.java new file mode 100644 index 00000000000..bc4c4f19cc0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MirrorStyleMaster.java @@ -0,0 +1,104 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.delayed.AtTheEndOfCombatDelayedTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.keyword.BackupAbility; +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.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.permanent.AttackingPredicate; +import mage.filter.predicate.permanent.ModifiedPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.targetpointer.FixedTargets; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MirrorStyleMaster extends CardImpl { + + public MirrorStyleMaster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}{R}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Backup 1 + BackupAbility backupAbility = new BackupAbility(this, 1); + this.addAbility(backupAbility); + + // Whenever this creature attacks, for each attacking modified creature you control, create a tapped and attacking token that's a copy of that creature. Exile those tokens at end of combat. + backupAbility.addAbility(new AttacksTriggeredAbility(new MirrorStyleMasterEffect()) + .setTriggerPhrase("Whenever this creature attacks, ")); + } + + private MirrorStyleMaster(final MirrorStyleMaster card) { + super(card); + } + + @Override + public MirrorStyleMaster copy() { + return new MirrorStyleMaster(this); + } +} + +class MirrorStyleMasterEffect extends OneShotEffect { + + private static final FilterPermanent filter = new FilterControlledCreaturePermanent(); + + static { + filter.add(AttackingPredicate.instance); + filter.add(ModifiedPredicate.instance); + } + + MirrorStyleMasterEffect() { + super(Outcome.Benefit); + staticText = "for each attacking modified creature you control, create a tapped and attacking token " + + "that's a copy of that creature. Exile those tokens at end of combat"; + } + + private MirrorStyleMasterEffect(final MirrorStyleMasterEffect effect) { + super(effect); + } + + @Override + public MirrorStyleMasterEffect copy() { + return new MirrorStyleMasterEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + List permanents = new ArrayList<>(); + for (Permanent permanent : game.getBattlefield().getActivePermanents( + filter, source.getControllerId(), source, game + )) { + CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect( + null, null, false, 1, true, true + ); + effect.setSavedPermanent(permanent); + effect.apply(game, source); + permanents.addAll(effect.getAddedPermanents()); + } + game.addDelayedTriggeredAbility(new AtTheEndOfCombatDelayedTriggeredAbility( + new ExileTargetEffect() + .setTargetPointer(new FixedTargets(permanents, game)) + .setText("exile those tokens") + ), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java index 1f2e3ce69e1..b94735d93c7 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java @@ -197,6 +197,7 @@ public final class MarchOfTheMachineCommander extends ExpansionSet { cards.add(new SetCardInfo("Mikaeus, the Lunarch", 197, Rarity.RARE, mage.cards.m.MikaeusTheLunarch.class)); cards.add(new SetCardInfo("Mind Stone", 364, Rarity.UNCOMMON, mage.cards.m.MindStone.class)); cards.add(new SetCardInfo("Mindless Automaton", 365, Rarity.UNCOMMON, mage.cards.m.MindlessAutomaton.class)); + cards.add(new SetCardInfo("Mirror-Style Master", 32, Rarity.RARE, mage.cards.m.MirrorStyleMaster.class)); cards.add(new SetCardInfo("Mortify", 337, Rarity.UNCOMMON, mage.cards.m.Mortify.class)); cards.add(new SetCardInfo("Mossfire Valley", 414, Rarity.RARE, mage.cards.m.MossfireValley.class)); cards.add(new SetCardInfo("Mosswort Bridge", 415, Rarity.RARE, mage.cards.m.MosswortBridge.class)); From 6a89a06ba067603c8218184782ad67e65b327540 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 12 Apr 2023 09:07:19 -0400 Subject: [PATCH 13/13] [MOC] Implement Saint Traft and Rem Karolus --- .../mage/cards/s/SaintTraftAndRemKarolus.java | 69 +++++++++++++++++++ .../mage/sets/MarchOfTheMachineCommander.java | 1 + 2 files changed, 70 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SaintTraftAndRemKarolus.java diff --git a/Mage.Sets/src/mage/cards/s/SaintTraftAndRemKarolus.java b/Mage.Sets/src/mage/cards/s/SaintTraftAndRemKarolus.java new file mode 100644 index 00000000000..6e590632a65 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SaintTraftAndRemKarolus.java @@ -0,0 +1,69 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BecomesTappedSourceTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.IfAbilityHasResolvedXTimesEffect; +import mage.abilities.effects.common.UntapSourceEffect; +import mage.abilities.keyword.ConvokeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterSpell; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.permanent.token.AngelToken; +import mage.game.permanent.token.RedHumanToken; +import mage.game.permanent.token.SpiritBlueToken; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SaintTraftAndRemKarolus extends CardImpl { + + private static final FilterSpell filter = new FilterSpell("a spell that has convoke"); + + static { + filter.add(new AbilityPredicate(ConvokeAbility.class)); + } + + public SaintTraftAndRemKarolus(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{R}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.HUMAN); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // Whenever Saint Traft and Rem Karolus becomes tapped, create a 1/1 red Human creature token if this is the first time this ability has resolved this turn. If it's the second time, create a 1/1 blue Spirit creature token with flying. If it's the third time, create a 4/4 white Angel creature token with flying. + Ability ability = new BecomesTappedSourceTriggeredAbility(new IfAbilityHasResolvedXTimesEffect( + Outcome.PutCreatureInPlay, 1, new CreateTokenEffect(new RedHumanToken()) + ).setText("create a 1/1 red Human creature token if this is the first time this ability has resolved this turn")); + ability.addEffect(new IfAbilityHasResolvedXTimesEffect( + Outcome.PutCreatureInPlay, 2, new CreateTokenEffect(new SpiritBlueToken()) + ).setText("If it's the second time, create a 1/1 blue Spirit creature token with flying")); + ability.addEffect(new IfAbilityHasResolvedXTimesEffect( + Outcome.PutCreatureInPlay, 3, new CreateTokenEffect(new AngelToken()) + ).setText("If it's the third time, create a 4/4 white Angel creature token with flying")); + this.addAbility(ability); + + // Whenever you cast a spell that has convoke, untap Saint Traft and Rem Karolus. + this.addAbility(new SpellCastControllerTriggeredAbility(new UntapSourceEffect(), filter, false)); + } + + private SaintTraftAndRemKarolus(final SaintTraftAndRemKarolus card) { + super(card); + } + + @Override + public SaintTraftAndRemKarolus copy() { + return new SaintTraftAndRemKarolus(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java index b94735d93c7..9ce52072423 100644 --- a/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java +++ b/Mage.Sets/src/mage/sets/MarchOfTheMachineCommander.java @@ -244,6 +244,7 @@ public final class MarchOfTheMachineCommander extends ExpansionSet { cards.add(new SetCardInfo("Root Out", 311, Rarity.COMMON, mage.cards.r.RootOut.class)); cards.add(new SetCardInfo("Saheeli's Artistry", 234, Rarity.RARE, mage.cards.s.SaheelisArtistry.class)); cards.add(new SetCardInfo("Saheeli, Sublime Artificer", 338, Rarity.UNCOMMON, mage.cards.s.SaheeliSublimeArtificer.class)); + cards.add(new SetCardInfo("Saint Traft and Rem Karolus", 9, Rarity.MYTHIC, mage.cards.s.SaintTraftAndRemKarolus.class)); cards.add(new SetCardInfo("Sandsteppe War Riders", 39, Rarity.RARE, mage.cards.s.SandsteppeWarRiders.class)); cards.add(new SetCardInfo("Schema Thief", 24, Rarity.RARE, mage.cards.s.SchemaThief.class)); cards.add(new SetCardInfo("Scrap Trawler", 373, Rarity.RARE, mage.cards.s.ScrapTrawler.class));