From 30d886f5f919d2d1f7a6c9397a4beef9be4e16e6 Mon Sep 17 00:00:00 2001 From: 4825764518 <100122841+4825764518@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:38:24 -0500 Subject: [PATCH 01/69] [CLU] Implement Suppressor Skyguard (#13351) --- .../src/mage/cards/s/SuppressorSkyguard.java | 89 +++++++++++++++++++ .../src/mage/sets/RavnicaClueEdition.java | 2 +- 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/s/SuppressorSkyguard.java diff --git a/Mage.Sets/src/mage/cards/s/SuppressorSkyguard.java b/Mage.Sets/src/mage/cards/s/SuppressorSkyguard.java new file mode 100644 index 00000000000..1c8289f4c59 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SuppressorSkyguard.java @@ -0,0 +1,89 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.PreventDamageToControllerEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; +import mage.game.combat.CombatGroup; +import mage.game.events.GameEvent; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +public class SuppressorSkyguard extends CardImpl { + public SuppressorSkyguard(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{U}"); + + this.addSubType(SubType.HUMAN); + this.addSubType(SubType.KNIGHT); + + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // whenever a player attacks you, if that player has another opponent who isn't being attacked, prevent all combat damage that would be dealt to you this combat + this.addAbility(new SuppressorSkyguardTriggerAttackYou()); + } + + public SuppressorSkyguard(SuppressorSkyguard card) { + super(card); + } + + @Override + public SuppressorSkyguard copy() { + return new SuppressorSkyguard(this); + } + + class SuppressorSkyguardTriggerAttackYou extends TriggeredAbilityImpl { + SuppressorSkyguardTriggerAttackYou() { + super(Zone.BATTLEFIELD, new PreventDamageToControllerEffect(Duration.EndOfCombat, true, false, Integer.MAX_VALUE), false); + this.setTriggerPhrase("whenever a player attacks you, "); + } + + private SuppressorSkyguardTriggerAttackYou(final SuppressorSkyguardTriggerAttackYou ability) { + super(ability); + } + + @Override + public SuppressorSkyguardTriggerAttackYou copy() { + return new SuppressorSkyguardTriggerAttackYou(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DEFENDER_ATTACKED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return getControllerId().equals(event.getTargetId()); + } + + @Override + public boolean checkInterveningIfClause(Game game) { + UUID activePlayerId = game.getActivePlayerId(); + Set opponents = game.getOpponents(activePlayerId); + game.getCombat() + .getGroups() + .stream() + .map(CombatGroup::getDefenderId) + .filter(Objects::nonNull) + .distinct() + .forEach(opponents::remove); + + return !opponents.isEmpty(); + } + + @Override + public String getRule() { + return "Whenever a player attacks you, if that player has another opponent who isn't being attacked, prevent all combat damage that would be dealt to you this combat."; + } + } +} diff --git a/Mage.Sets/src/mage/sets/RavnicaClueEdition.java b/Mage.Sets/src/mage/sets/RavnicaClueEdition.java index 982b9ff303d..e2a5ab8397e 100644 --- a/Mage.Sets/src/mage/sets/RavnicaClueEdition.java +++ b/Mage.Sets/src/mage/sets/RavnicaClueEdition.java @@ -253,7 +253,7 @@ public final class RavnicaClueEdition extends ExpansionSet { cards.add(new SetCardInfo("Stunt Double", 101, Rarity.RARE, mage.cards.s.StuntDouble.class)); cards.add(new SetCardInfo("Sumala Rumblers", 45, Rarity.UNCOMMON, mage.cards.s.SumalaRumblers.class)); cards.add(new SetCardInfo("Sunhome Stalwart", 73, Rarity.UNCOMMON, mage.cards.s.SunhomeStalwart.class)); - //cards.add(new SetCardInfo("Suppressor Skyguard", 46, Rarity.RARE, mage.cards.s.SuppressorSkyguard.class)); + cards.add(new SetCardInfo("Suppressor Skyguard", 46, Rarity.RARE, mage.cards.s.SuppressorSkyguard.class)); cards.add(new SetCardInfo("Supreme Verdict", 211, Rarity.RARE, mage.cards.s.SupremeVerdict.class)); cards.add(new SetCardInfo("Swamp", 262, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Swamp", 263, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); From 3be8e3879c1d173cbe3f99fdedb753991390826e Mon Sep 17 00:00:00 2001 From: 4825764518 <100122841+4825764518@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:54:22 -0500 Subject: [PATCH 02/69] [ACR] Implement Alexios, Deimos of Kosmos (#13346) --- .../mage/cards/a/AlexiosDeimosOfKosmos.java | 97 +++++++++++++++++++ Mage.Sets/src/mage/sets/AssassinsCreed.java | 6 +- 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/a/AlexiosDeimosOfKosmos.java diff --git a/Mage.Sets/src/mage/cards/a/AlexiosDeimosOfKosmos.java b/Mage.Sets/src/mage/cards/a/AlexiosDeimosOfKosmos.java new file mode 100644 index 00000000000..7fa2796d4de --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AlexiosDeimosOfKosmos.java @@ -0,0 +1,97 @@ +package mage.cards.a; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksEachCombatStaticAbility; +import mage.abilities.effects.*; +import mage.abilities.effects.common.TargetPlayerGainControlSourceEffect; +import mage.abilities.effects.common.UntapSourceEffect; +import mage.abilities.effects.common.continuous.CantBeSacrificedSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; + +import java.util.Objects; +import java.util.UUID; + +public class AlexiosDeimosOfKosmos extends CardImpl { + public AlexiosDeimosOfKosmos(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.BERSERKER); + + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // attacks each combat if able, can't be sacrificed, and can't attack it's owner + Ability ability = new AttacksEachCombatStaticAbility(); + ability.addEffect(new CantBeSacrificedSourceEffect().setText(", can't be sacrificed")); + ability.addEffect(new AlexiosDeimosOfKosmosRestrictionEffect()); + this.addAbility(ability); + + // at the beginning of each player's upkeep, that player gains control of alexios, untaps it, and puts a +1/+1 counter on it. it gains haste until end of turn + Ability upkeepAbility = new BeginningOfUpkeepTriggeredAbility(TargetController.EACH_PLAYER, null, false); + upkeepAbility.addEffect(new TargetPlayerGainControlSourceEffect().setText("that player gains control of Alexios")); + upkeepAbility.addEffect(new UntapSourceEffect().setText(", untaps it")); + upkeepAbility.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(1)).setText(", and puts a +1/+1 counter on it")); + upkeepAbility.addEffect(new GainAbilitySourceEffect(HasteAbility.getInstance(), Duration.EndOfTurn).setText("it gains haste until end of turn")); + this.addAbility(upkeepAbility); + } + + public AlexiosDeimosOfKosmos(CardImpl card) { + super(card); + } + + @Override + public AlexiosDeimosOfKosmos copy() { + return new AlexiosDeimosOfKosmos(this); + } + + class AlexiosDeimosOfKosmosRestrictionEffect extends RestrictionEffect { + public AlexiosDeimosOfKosmosRestrictionEffect() { + super(Duration.WhileOnBattlefield); + this.staticText = ", and can't attack it's owner"; + } + + public AlexiosDeimosOfKosmosRestrictionEffect(RestrictionEffect effect) { + super(effect); + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + return Objects.equals(permanent.getId(), source.getSourceId()); + } + + @Override + public AlexiosDeimosOfKosmosRestrictionEffect copy() { + return new AlexiosDeimosOfKosmosRestrictionEffect(this); + } + + @Override + public boolean canAttack(Permanent attacker, UUID defenderId, Ability source, Game game, boolean canUseChooseDialogs) { + if (defenderId == null) { + return true; + } + + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + return true; + } + + UUID ownerId = permanent.getOwnerId(); + return !defenderId.equals(ownerId); + } + } +} diff --git a/Mage.Sets/src/mage/sets/AssassinsCreed.java b/Mage.Sets/src/mage/sets/AssassinsCreed.java index dbd20c8865e..2dcb67a87d1 100644 --- a/Mage.Sets/src/mage/sets/AssassinsCreed.java +++ b/Mage.Sets/src/mage/sets/AssassinsCreed.java @@ -30,9 +30,9 @@ public final class AssassinsCreed extends ExpansionSet { cards.add(new SetCardInfo("Adewale, Breaker of Chains", 44, Rarity.UNCOMMON, mage.cards.a.AdewaleBreakerOfChains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Adrestia", 252, Rarity.UNCOMMON, mage.cards.a.Adrestia.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Adrestia", 68, Rarity.UNCOMMON, mage.cards.a.Adrestia.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Alexios, Deimos of Kosmos", 134, Rarity.UNCOMMON, mage.cards.a.AlexiosDeimosOfKosmos.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Alexios, Deimos of Kosmos", 214, Rarity.UNCOMMON, mage.cards.a.AlexiosDeimosOfKosmos.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Alexios, Deimos of Kosmos", 33, Rarity.UNCOMMON, mage.cards.a.AlexiosDeimosOfKosmos.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Alexios, Deimos of Kosmos", 134, Rarity.UNCOMMON, mage.cards.a.AlexiosDeimosOfKosmos.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Alexios, Deimos of Kosmos", 214, Rarity.UNCOMMON, mage.cards.a.AlexiosDeimosOfKosmos.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Alexios, Deimos of Kosmos", 33, Rarity.UNCOMMON, mage.cards.a.AlexiosDeimosOfKosmos.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Altair Ibn-La'Ahad", 137, Rarity.MYTHIC, mage.cards.a.AltairIbnLaAhad.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Altair Ibn-La'Ahad", 225, Rarity.MYTHIC, mage.cards.a.AltairIbnLaAhad.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Altair Ibn-La'Ahad", 268, Rarity.MYTHIC, mage.cards.a.AltairIbnLaAhad.class, NON_FULL_USE_VARIOUS)); From 2853cf1b9921ee08270ee039b52c458d3f5149c5 Mon Sep 17 00:00:00 2001 From: Johannes Wolf <519002+johannes-wolf@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:52:20 +0100 Subject: [PATCH 03/69] Grist, the Hunger Tide and other - added card hints (#13354) Grist, the Hunger Tide Songs of the Damned Season of Loss --- Mage.Sets/src/mage/cards/g/GristTheHungerTide.java | 9 +++++---- Mage.Sets/src/mage/cards/s/SeasonOfLoss.java | 6 ++++-- Mage.Sets/src/mage/cards/s/SongsOfTheDamned.java | 7 +++++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Mage.Sets/src/mage/cards/g/GristTheHungerTide.java b/Mage.Sets/src/mage/cards/g/GristTheHungerTide.java index bae6785d8e7..2c22115dba2 100644 --- a/Mage.Sets/src/mage/cards/g/GristTheHungerTide.java +++ b/Mage.Sets/src/mage/cards/g/GristTheHungerTide.java @@ -13,6 +13,7 @@ import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DestroyTargetEffect; import mage.abilities.effects.common.DoWhenCostPaid; import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; @@ -34,9 +35,6 @@ import java.util.UUID; * @author TheElk801 */ public final class GristTheHungerTide extends CardImpl { - - private static final DynamicValue xValue = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURES); - public GristTheHungerTide(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{1}{B}{G}"); @@ -61,7 +59,10 @@ public final class GristTheHungerTide extends CardImpl { ), -2)); // −5: Each opponent loses life equal to the number of creature cards in your graveyard. - this.addAbility(new LoyaltyAbility(new LoseLifeOpponentsEffect(xValue).setText("each opponent loses life equal to the number of creature cards in your graveyard"), -5)); + DynamicValue creatureCardsInGraveyard = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURES); + this.addAbility(new LoyaltyAbility(new LoseLifeOpponentsEffect(creatureCardsInGraveyard) + .setText("each opponent loses life equal to the number of creature cards in your graveyard"), -5) + .addHint(new ValueHint("Creature cards in your graveyard", creatureCardsInGraveyard))); } private GristTheHungerTide(final GristTheHungerTide card) { diff --git a/Mage.Sets/src/mage/cards/s/SeasonOfLoss.java b/Mage.Sets/src/mage/cards/s/SeasonOfLoss.java index 0d9a6b36393..edb864fad0b 100644 --- a/Mage.Sets/src/mage/cards/s/SeasonOfLoss.java +++ b/Mage.Sets/src/mage/cards/s/SeasonOfLoss.java @@ -8,6 +8,7 @@ import mage.abilities.effects.Effect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.LoseLifeOpponentsEffect; import mage.abilities.effects.common.SacrificeAllEffect; +import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -22,7 +23,6 @@ import java.util.UUID; * @author jimga150 */ public final class SeasonOfLoss extends CardImpl { - public SeasonOfLoss(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}{B}"); @@ -41,9 +41,11 @@ public final class SeasonOfLoss extends CardImpl { 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. - Mode mode3 = new Mode(new LoseLifeOpponentsEffect(new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURES)) + DynamicValue creatureCardsInGraveyard = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURES); + Mode mode3 = new Mode(new LoseLifeOpponentsEffect(creatureCardsInGraveyard) .setText("Each opponent loses X life, where X is the number of creature cards in your graveyard.")); this.getSpellAbility().addMode(mode3.withPawPrintValue(3)); + this.getSpellAbility().addHint(new ValueHint("Creature cards in your graveyard", creatureCardsInGraveyard)); } private SeasonOfLoss(final SeasonOfLoss card) { diff --git a/Mage.Sets/src/mage/cards/s/SongsOfTheDamned.java b/Mage.Sets/src/mage/cards/s/SongsOfTheDamned.java index f6b55abecc3..cebb607988d 100644 --- a/Mage.Sets/src/mage/cards/s/SongsOfTheDamned.java +++ b/Mage.Sets/src/mage/cards/s/SongsOfTheDamned.java @@ -2,8 +2,10 @@ package mage.cards.s; import java.util.UUID; import mage.Mana; +import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; import mage.abilities.effects.mana.DynamicManaEffect; +import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -14,13 +16,14 @@ import mage.filter.StaticFilters; * @author fireshoes */ public final class SongsOfTheDamned extends CardImpl { - public SongsOfTheDamned(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{B}"); // Add {B} for each creature card in your graveyard. - DynamicManaEffect effect = new DynamicManaEffect(Mana.BlackMana(1), new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURE)); + DynamicValue creatureCardsInGraveyard = new CardsInControllerGraveyardCount(StaticFilters.FILTER_CARD_CREATURES); + DynamicManaEffect effect = new DynamicManaEffect(Mana.BlackMana(1), creatureCardsInGraveyard); this.getSpellAbility().addEffect(effect); + this.getSpellAbility().addHint(new ValueHint("Creature cards in your graveyard", creatureCardsInGraveyard)); } private SongsOfTheDamned(final SongsOfTheDamned card) { From 216826b513b8794e9dd1e85ff968befe8ef5c9ff Mon Sep 17 00:00:00 2001 From: Johannes Wolf <519002+johannes-wolf@users.noreply.github.com> Date: Sun, 16 Feb 2025 20:33:47 +0100 Subject: [PATCH 04/69] GUI, game: fixed game error in choose ability dialog on using CTRL and mouse wheel (#13186) --- .../components/ability/AbilityPicker.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/components/ability/AbilityPicker.java b/Mage.Client/src/main/java/mage/client/components/ability/AbilityPicker.java index 66ddd3135d9..77b75077791 100644 --- a/Mage.Client/src/main/java/mage/client/components/ability/AbilityPicker.java +++ b/Mage.Client/src/main/java/mage/client/components/ability/AbilityPicker.java @@ -152,7 +152,7 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener { setBackgroundPainter(mwPanelPainter); title = new ColorPane(); - title.setFont(new Font("Times New Roman", 1, sizeMod(15))); + title.setFont(new Font("Times New Roman", Font.BOLD, sizeMod(15))); title.setEditable(false); title.setFocusCycleRoot(false); title.setOpaque(false); @@ -186,11 +186,12 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener { rows.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent evt) { - if (SwingUtilities.isLeftMouseButton(evt)) { + if (SwingUtilities.isLeftMouseButton(evt) && !rows.isSelectionEmpty()) { objectMouseClicked(evt); } } }); + rows.setSelectedIndex(0); rows.setFont(new Font("Times New Roman", 1, sizeMod(17))); rows.setBorder(BorderFactory.createEmptyBorder()); @@ -233,18 +234,16 @@ public class AbilityPicker extends JXPanel implements MouseWheelListener { @Override public void mouseWheelMoved(MouseWheelEvent e) { - int notches = e.getWheelRotation(); - int index = rows.getSelectedIndex(); - - if (notches < 0) { - if (index > 0) { - rows.setSelectedIndex(index - 1); - rows.repaint(); - } - } else if (index < choices.size() - 1) { - rows.setSelectedIndex(index + 1); - rows.repaint(); + int direction = e.getWheelRotation() < 0 ? -1 : +1; + int index = rows.getSelectedIndex() + direction; + if (index < 0) { + index = 0; + } else if (index >= choices.size()) { + index = choices.size() - 1; } + + rows.setSelectedIndex(index); + rows.repaint(); } private void objectMouseClicked(MouseEvent event) { From fe52d824b4f532b01422c47f3aad7af94230feac Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Mon, 17 Feb 2025 00:23:48 +0400 Subject: [PATCH 05/69] GUI: fixed error on mouse move over some table headers (close #13331, close #13282, close #13272, close #13256, close #13246, close #13163, close #13107) --- Mage.Common/src/main/java/mage/components/table/MageTable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Common/src/main/java/mage/components/table/MageTable.java b/Mage.Common/src/main/java/mage/components/table/MageTable.java index 86c8a5fae90..584a95e5f6a 100644 --- a/Mage.Common/src/main/java/mage/components/table/MageTable.java +++ b/Mage.Common/src/main/java/mage/components/table/MageTable.java @@ -64,10 +64,10 @@ public class MageTable extends JTable { // html tooltip java.awt.Point p = e.getPoint(); int colIndex = columnModel.getColumnIndexAtX(p.x); - TableColumn col = columnModel.getColumn(colIndex); if (colIndex < 0) { return ""; } + TableColumn col = columnModel.getColumn(colIndex); int realIndex = col.getModelIndex(); String tip; From df0a2760b63469c3550dd7a89f69089cd733b697 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Mon, 17 Feb 2025 01:16:04 +0400 Subject: [PATCH 06/69] refactor: fixed wrong usage of getModes (related to #13338) --- Mage.Sets/src/mage/cards/m/Mistfolk.java | 12 ++++-------- Mage.Sets/src/mage/cards/w/WallOfShadows.java | 3 ++- Mage/src/main/java/mage/constants/SubType.java | 2 +- .../mage/game/command/emblems/DackFaydenEmblem.java | 3 ++- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Mage.Sets/src/mage/cards/m/Mistfolk.java b/Mage.Sets/src/mage/cards/m/Mistfolk.java index 60ba8fc1622..44b55bda1e4 100644 --- a/Mage.Sets/src/mage/cards/m/Mistfolk.java +++ b/Mage.Sets/src/mage/cards/m/Mistfolk.java @@ -2,7 +2,6 @@ package mage.cards.m; import mage.MageInt; import mage.abilities.Ability; -import mage.abilities.Mode; import mage.abilities.SpellAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.mana.ManaCostsImpl; @@ -17,8 +16,8 @@ import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.stack.Spell; -import mage.target.Target; import mage.target.TargetSpell; +import mage.util.CardUtil; import java.util.UUID; @@ -69,13 +68,10 @@ enum MistfolkPredicate implements ObjectSourcePlayerPredicate { if (sourceObject == null || input.getObject() == null) { return false; } + for (SpellAbility spellAbility : input.getObject().getSpellAbilities()) { - for (Mode mode : spellAbility.getModes().values()) { - for (Target target : spellAbility.getTargets()) { - if (target.getTargets().contains(input.getSourceId())) { - return true; - } - } + if (CardUtil.getAllSelectedTargets(spellAbility, game).contains(input.getSourceId())) { + return true; } } return false; diff --git a/Mage.Sets/src/mage/cards/w/WallOfShadows.java b/Mage.Sets/src/mage/cards/w/WallOfShadows.java index 65ffc118058..f5df95b7287 100644 --- a/Mage.Sets/src/mage/cards/w/WallOfShadows.java +++ b/Mage.Sets/src/mage/cards/w/WallOfShadows.java @@ -77,7 +77,8 @@ enum CanTargetOnlyWallsPredicate implements Predicate { return false; } boolean canTargetOnlyWalls = false; - for (Mode mode : stackObject.getStackAbility().getModes().values()) { + for (UUID modeId : stackObject.getStackAbility().getModes().getSelectedModes()) { + Mode mode = stackObject.getStackAbility().getModes().get(modeId); for (Target target : mode.getTargets()) { Filter filter = target.getFilter(); if (!(filter instanceof FilterPermanent)) { diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index ed6bcf983b1..5037f60d44a 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -551,7 +551,7 @@ public enum SubType { @Override public String toString() { - return "Subtype(" + subtype + ')'; + return "Subtype(" + subtype + ')'; // warning, do not change until refactor code like predicate.toString().equals } } diff --git a/Mage/src/main/java/mage/game/command/emblems/DackFaydenEmblem.java b/Mage/src/main/java/mage/game/command/emblems/DackFaydenEmblem.java index 0a2dabc7efb..faa2636f36d 100644 --- a/Mage/src/main/java/mage/game/command/emblems/DackFaydenEmblem.java +++ b/Mage/src/main/java/mage/game/command/emblems/DackFaydenEmblem.java @@ -70,7 +70,8 @@ class DackFaydenEmblemTriggeredAbility extends TriggeredAbilityImpl { Spell spell = game.getStack().getSpell(event.getTargetId()); if (spell != null) { SpellAbility spellAbility = spell.getSpellAbility(); - for (Mode mode : spellAbility.getModes().values()) { + for (UUID modeId : spellAbility.getModes().getSelectedModes()) { + Mode mode = spellAbility.getModes().get(modeId); for (Target target : mode.getTargets()) { if (!target.isNotTarget()) { for (UUID targetId : target.getTargets()) { From 8d541457326f70f85cd00f656cbf1aefcec50771 Mon Sep 17 00:00:00 2001 From: jam736 <33673020+jam736@users.noreply.github.com> Date: Sun, 16 Feb 2025 18:06:08 -0500 Subject: [PATCH 07/69] [DFT] Implement Ancient Vendetta (#13344) --- .../src/mage/cards/a/AncientVendetta.java | 60 +++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 1 + 2 files changed, 61 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AncientVendetta.java diff --git a/Mage.Sets/src/mage/cards/a/AncientVendetta.java b/Mage.Sets/src/mage/cards/a/AncientVendetta.java new file mode 100644 index 00000000000..13e219717df --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AncientVendetta.java @@ -0,0 +1,60 @@ +package mage.cards.a; + +import mage.abilities.Ability; +import mage.abilities.effects.common.ChooseACardNameEffect; +import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.game.Game; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + + +/** + * + * @author jam736 + */ +public final class AncientVendetta extends CardImpl { + + public AncientVendetta(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}"); + + // Choose a card name. Search target opponent’s graveyard, hand, and library for up to four cards with that name and exile them. Then that player shuffles. + this.getSpellAbility().addEffect((new ChooseACardNameEffect(ChooseACardNameEffect.TypeOfName.ALL))); + this.getSpellAbility().addEffect(new AncientVendettaEffect()); + this.getSpellAbility().addTarget(new TargetOpponent()); + } + + private AncientVendetta(final AncientVendetta card) { + super(card); + } + + @Override + public AncientVendetta copy() { + return new AncientVendetta(this); + } +} + +class AncientVendettaEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect { + + AncientVendettaEffect() { + super(true, "target opponent's", "up to four cards with that name", false, 4); + } + + private AncientVendettaEffect(final AncientVendettaEffect effect) { + super(effect); + } + + @Override + public AncientVendettaEffect copy() { + return new AncientVendettaEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + String chosenCardName = (String) game.getState().getValue(source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY); + return applySearchAndExile(game, source, chosenCardName, getTargetPointer().getFirst(game, source)); + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 9e5c09d6335..dfcef6ecbec 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -30,6 +30,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Air Response Unit", 1, Rarity.UNCOMMON, mage.cards.a.AirResponseUnit.class)); cards.add(new SetCardInfo("Alacrian Jaguar", 152, Rarity.COMMON, mage.cards.a.AlacrianJaguar.class)); cards.add(new SetCardInfo("Amonkhet Raceway", 248, Rarity.UNCOMMON, mage.cards.a.AmonkhetRaceway.class)); + cards.add(new SetCardInfo("Ancient Vendetta", 75, Rarity.UNCOMMON, mage.cards.a.AncientVendetta.class)); cards.add(new SetCardInfo("Apocalypse Runner", 188, Rarity.UNCOMMON, mage.cards.a.ApocalypseRunner.class)); cards.add(new SetCardInfo("Autarch Mammoth", 153, Rarity.UNCOMMON, mage.cards.a.AutarchMammoth.class)); cards.add(new SetCardInfo("Avishkar Raceway", 249, Rarity.COMMON, mage.cards.a.AvishkarRaceway.class)); From 0281b35c589d95212c62d3e1c87646b9ceb4d72f Mon Sep 17 00:00:00 2001 From: padfoothelix Date: Mon, 17 Feb 2025 13:24:05 +0100 Subject: [PATCH 08/69] [WHO] Implement The Girl in the Fireplace (#13356) --- .../sources/ScryfallImageSupportTokens.java | 2 + .../mage/cards/t/TheGirlInTheFireplace.java | 105 ++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 4 +- .../TheGirlInTheFireplaceHorseToken.java | 36 ++++++ .../TheGirlInTheFireplaceHumanNobleToken.java | 40 +++++++ 5 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/t/TheGirlInTheFireplace.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/TheGirlInTheFireplaceHorseToken.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/TheGirlInTheFireplaceHumanNobleToken.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java index a5fe3be7c12..e0abefff53d 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportTokens.java @@ -2169,6 +2169,8 @@ public class ScryfallImageSupportTokens { // WHO put("WHO/Alien Insect", "https://api.scryfall.com/cards/twho/19/en?format=image"); + put("WHO/Human Noble", "https://api.scryfall.com/cards/twho/7/en?format=image"); + put("WHO/Horse", "https://api.scryfall.com/cards/twho/4/en?format=image"); // 8ED put("8ED/Rukh", "https://api.scryfall.com/cards/p03/7/en?format=image"); diff --git a/Mage.Sets/src/mage/cards/t/TheGirlInTheFireplace.java b/Mage.Sets/src/mage/cards/t/TheGirlInTheFireplace.java new file mode 100644 index 00000000000..e8e7af87461 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheGirlInTheFireplace.java @@ -0,0 +1,105 @@ +package mage.cards.t; + +import java.util.UUID; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.common.SagaAbility; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; +import mage.abilities.effects.common.counter.TimeTravelEffect; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SagaChapter; +import mage.game.Game; +import mage.game.events.DamagedPlayerEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.TheGirlInTheFireplaceHorseToken; +import mage.game.permanent.token.TheGirlInTheFireplaceHumanNobleToken; +import mage.filter.common.FilterControlledCreaturePermanent; + +/** + * + * @author padfoot + */ +public final class TheGirlInTheFireplace extends CardImpl { + + + public TheGirlInTheFireplace(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}"); + + this.subtype.add(SubType.SAGA); + + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.) + SagaAbility sagaAbility = new SagaAbility(this); + + // I -- Create a 1/1 white Human Noble creature token with vanishing 3 and "Prevent all damage that would be dealt to this creature." + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I, new CreateTokenEffect(new TheGirlInTheFireplaceHumanNobleToken())); + + // II -- Create a 2/2 white Horse creature token with "Doctors you control have horsemanship." + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_II, new CreateTokenEffect(new TheGirlInTheFireplaceHorseToken()) + .withAdditionalRules(" (They can't be blocked except by creatures with horsemanship.)")); + + // III -- Whenever a creature you control deals combat damage to a player this turn, time travel. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_III, + new CreateDelayedTriggeredAbilityEffect( + new TheGirlInTheFireplaceTriggeredAbility() + ) + ); + + this.addAbility(sagaAbility); + + } + + private TheGirlInTheFireplace(final TheGirlInTheFireplace card) { + super(card); + } + + @Override + public TheGirlInTheFireplace copy() { + return new TheGirlInTheFireplace(this); + } +} + +class TheGirlInTheFireplaceTriggeredAbility extends DelayedTriggeredAbility { + + private static final FilterControlledCreaturePermanent filter = new FilterControlledCreaturePermanent(); + + public TheGirlInTheFireplaceTriggeredAbility() { + super(new TimeTravelEffect(), Duration.EndOfTurn, false); + } + + private TheGirlInTheFireplaceTriggeredAbility(TheGirlInTheFireplaceTriggeredAbility ability) { + super(ability); + } + + @Override + public TheGirlInTheFireplaceTriggeredAbility copy() { + return new TheGirlInTheFireplaceTriggeredAbility(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 (!((DamagedPlayerEvent) event).isCombatDamage()) { + return false; + } + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getSourceId()); + if (!filter.match(permanent, getControllerId(), this, game)) { + return false; + } + return true; + } + + @Override + public String getRule() { + return "Whenever a creature you control deals combat damage to a player this turn, time travel."; + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index a9ff63f658b..e0a8dc21424 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -955,8 +955,8 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("The Fugitive Doctor", 417, Rarity.RARE, mage.cards.t.TheFugitiveDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Fugitive Doctor", 541, Rarity.RARE, mage.cards.t.TheFugitiveDoctor.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Fugitive Doctor", 735, Rarity.RARE, mage.cards.t.TheFugitiveDoctor.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The Girl in the Fireplace", 21, Rarity.RARE, mage.cards.t.TheGirlInTheFireplace.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("The Girl in the Fireplace", 626, Rarity.RARE, mage.cards.t.TheGirlInTheFireplace.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Girl in the Fireplace", 21, Rarity.RARE, mage.cards.t.TheGirlInTheFireplace.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Girl in the Fireplace", 626, Rarity.RARE, mage.cards.t.TheGirlInTheFireplace.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("The Lux Foundation Library", 588, Rarity.COMMON, mage.cards.t.TheLuxFoundationLibrary.class)); //cards.add(new SetCardInfo("The Master, Formed Anew", 1017, Rarity.RARE, mage.cards.t.TheMasterFormedAnew.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("The Master, Formed Anew", 1133, Rarity.RARE, mage.cards.t.TheMasterFormedAnew.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage/src/main/java/mage/game/permanent/token/TheGirlInTheFireplaceHorseToken.java b/Mage/src/main/java/mage/game/permanent/token/TheGirlInTheFireplaceHorseToken.java new file mode 100644 index 00000000000..217f816d085 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/TheGirlInTheFireplaceHorseToken.java @@ -0,0 +1,36 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.HorsemanshipAbility; +import mage.constants.Duration; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.common.FilterCreaturePermanent; + +/** + * @author padfoot + */ +public final class TheGirlInTheFireplaceHorseToken extends TokenImpl { + + public TheGirlInTheFireplaceHorseToken() { + super("Horse Token", "2/2 white Horse creature token with \"Doctors you control have horsemanship.\""); + cardType.add(CardType.CREATURE); + color.setWhite(true); + subtype.add(SubType.HORSE); + power = new MageInt(2); + toughness = new MageInt(2); + this.addAbility(new SimpleStaticAbility(new GainAbilityControlledEffect(HorsemanshipAbility.getInstance(), + Duration.WhileOnBattlefield, new FilterCreaturePermanent(SubType.DOCTOR, "Doctors"), false))); + } + + private TheGirlInTheFireplaceHorseToken(final TheGirlInTheFireplaceHorseToken token) { + super(token); + } + + @Override + public TheGirlInTheFireplaceHorseToken copy() { + return new TheGirlInTheFireplaceHorseToken(this); + } +} diff --git a/Mage/src/main/java/mage/game/permanent/token/TheGirlInTheFireplaceHumanNobleToken.java b/Mage/src/main/java/mage/game/permanent/token/TheGirlInTheFireplaceHumanNobleToken.java new file mode 100644 index 00000000000..7e19bb8edc5 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/TheGirlInTheFireplaceHumanNobleToken.java @@ -0,0 +1,40 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.PreventDamageToSourceEffect; +import mage.abilities.keyword.VanishingAbility; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Duration; + +/** + * @author padfoot + */ +public final class TheGirlInTheFireplaceHumanNobleToken extends TokenImpl { + + public TheGirlInTheFireplaceHumanNobleToken() { + super("Human Noble Token", "1/1 white Human Noble creature token with vanishing 3 and \"Prevent all damage that would be dealt to this creature.\""); + cardType.add(CardType.CREATURE); + color.setWhite(true); + subtype.add(SubType.HUMAN,SubType.NOBLE); + power = new MageInt(1); + toughness = new MageInt(1); + this.addAbility(new VanishingAbility(3)); + this.addAbility(new SimpleStaticAbility( + new PreventDamageToSourceEffect( + Duration.WhileOnBattlefield, + Integer.MAX_VALUE + ).setText("Prevent all damage that would be dealt to this creature.") + )); + } + + private TheGirlInTheFireplaceHumanNobleToken(final TheGirlInTheFireplaceHumanNobleToken token) { + super(token); + } + + @Override + public TheGirlInTheFireplaceHumanNobleToken copy() { + return new TheGirlInTheFireplaceHumanNobleToken(this); + } +} From 91a648c50d9a487cef25c151e01a0e26f0113bf5 Mon Sep 17 00:00:00 2001 From: 4825764518 <100122841+4825764518@users.noreply.github.com> Date: Mon, 17 Feb 2025 19:57:55 -0500 Subject: [PATCH 09/69] [ACR] Implement Viewpoint Synchronization (#13358) --- Mage.Sets/src/mage/cards/c/Cultivate.java | 4 +- .../src/mage/cards/f/FlareOfCultivation.java | 4 +- .../src/mage/cards/f/FlourishingBloomKin.java | 4 +- Mage.Sets/src/mage/cards/k/KodamasReach.java | 4 +- Mage.Sets/src/mage/cards/n/NavigationOrb.java | 4 +- .../src/mage/cards/n/NissasPilgrimage.java | 6 +-- Mage.Sets/src/mage/cards/p/Peregrination.java | 4 +- .../cards/v/ViewpointSynchronization.java | 32 ++++++++++++++++ Mage.Sets/src/mage/sets/AssassinsCreed.java | 4 +- ...ntoBattlefieldTappedRestInHandEffect.java} | 37 ++++++++++++------- 10 files changed, 73 insertions(+), 30 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/v/ViewpointSynchronization.java rename Mage/src/main/java/mage/abilities/effects/common/search/{SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect.java => SearchLibraryPutOntoBattlefieldTappedRestInHandEffect.java} (53%) diff --git a/Mage.Sets/src/mage/cards/c/Cultivate.java b/Mage.Sets/src/mage/cards/c/Cultivate.java index db5b9a96978..aec124c0685 100644 --- a/Mage.Sets/src/mage/cards/c/Cultivate.java +++ b/Mage.Sets/src/mage/cards/c/Cultivate.java @@ -1,6 +1,6 @@ package mage.cards.c; -import mage.abilities.effects.common.search.SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; import mage.cards.*; import mage.constants.CardType; import mage.filter.StaticFilters; @@ -17,7 +17,7 @@ public final class Cultivate extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{G}"); // Search your library for up to two basic land cards, reveal those cards, put one onto the battlefield tapped and the other into your hand, then shuffle. - this.getSpellAbility().addEffect(new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect( + this.getSpellAbility().addEffect(new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect( new TargetCardInLibrary(0, 2, StaticFilters.FILTER_CARD_BASIC_LANDS))); } diff --git a/Mage.Sets/src/mage/cards/f/FlareOfCultivation.java b/Mage.Sets/src/mage/cards/f/FlareOfCultivation.java index a69263f3436..fd4ae247357 100644 --- a/Mage.Sets/src/mage/cards/f/FlareOfCultivation.java +++ b/Mage.Sets/src/mage/cards/f/FlareOfCultivation.java @@ -3,7 +3,7 @@ package mage.cards.f; import mage.ObjectColor; import mage.abilities.costs.AlternativeCostSourceAbility; import mage.abilities.costs.common.SacrificeTargetCost; -import mage.abilities.effects.common.search.SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -35,7 +35,7 @@ public final class FlareOfCultivation extends CardImpl { this.addAbility(new AlternativeCostSourceAbility(new SacrificeTargetCost(filter)).setRuleAtTheTop(true)); // Search your library for up to two basic land cards, reveal those cards, put one onto the battlefield tapped and the other into your hand, then shuffle. - this.getSpellAbility().addEffect(new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect( + this.getSpellAbility().addEffect(new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect( new TargetCardInLibrary(0, 2, StaticFilters.FILTER_CARD_BASIC_LANDS) )); } diff --git a/Mage.Sets/src/mage/cards/f/FlourishingBloomKin.java b/Mage.Sets/src/mage/cards/f/FlourishingBloomKin.java index 66aa3e6824d..49f20b54d04 100644 --- a/Mage.Sets/src/mage/cards/f/FlourishingBloomKin.java +++ b/Mage.Sets/src/mage/cards/f/FlourishingBloomKin.java @@ -9,7 +9,7 @@ import mage.abilities.common.TurnedFaceUpSourceTriggeredAbility; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; import mage.abilities.effects.common.continuous.BoostSourceEffect; -import mage.abilities.effects.common.search.SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; import mage.abilities.hint.Hint; import mage.abilities.hint.ValueHint; import mage.constants.Duration; @@ -56,7 +56,7 @@ public final class FlourishingBloomKin extends CardImpl { // When Flourishing Bloom-Kin is turned face up, search your library for up to two Forest cards and reveal them. // Put one of them onto the battlefield tapped and the other into your hand, then shuffle. this.addAbility(new TurnedFaceUpSourceTriggeredAbility( - new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(new TargetCardInLibrary(0, 2, filterForestCards)) + new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(new TargetCardInLibrary(0, 2, filterForestCards)) .setText("search your library for up to two Forest cards and reveal them. Put one of them onto the battlefield tapped and the other into your hand, then shuffle"))); } diff --git a/Mage.Sets/src/mage/cards/k/KodamasReach.java b/Mage.Sets/src/mage/cards/k/KodamasReach.java index c7544159381..79f14925c3d 100644 --- a/Mage.Sets/src/mage/cards/k/KodamasReach.java +++ b/Mage.Sets/src/mage/cards/k/KodamasReach.java @@ -1,6 +1,6 @@ package mage.cards.k; -import mage.abilities.effects.common.search.SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; import mage.cards.*; import mage.constants.CardType; import mage.constants.SubType; @@ -19,7 +19,7 @@ public final class KodamasReach extends CardImpl { this.subtype.add(SubType.ARCANE); // Search your library for up to two basic land cards, reveal those cards, put one onto the battlefield tapped and the other into your hand, then shuffle. - this.getSpellAbility().addEffect(new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect( + this.getSpellAbility().addEffect(new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect( new TargetCardInLibrary(0, 2, StaticFilters.FILTER_CARD_BASIC_LANDS))); } diff --git a/Mage.Sets/src/mage/cards/n/NavigationOrb.java b/Mage.Sets/src/mage/cards/n/NavigationOrb.java index 799b476eba3..57eab146ae4 100644 --- a/Mage.Sets/src/mage/cards/n/NavigationOrb.java +++ b/Mage.Sets/src/mage/cards/n/NavigationOrb.java @@ -5,7 +5,7 @@ import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.SacrificeSourceCost; import mage.abilities.costs.common.TapSourceCost; import mage.abilities.costs.mana.GenericManaCost; -import mage.abilities.effects.common.search.SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; import mage.cards.*; import mage.constants.*; import mage.filter.FilterCard; @@ -32,7 +32,7 @@ public final class NavigationOrb extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}"); // {2}, {T}, Sacrifice Navigation Orb: Search your library for up to two basic land cards and/or Gate cards, reveal those cards, put one onto the battlefield tapped and the other into your hand, then shuffle. - Ability ability = new SimpleActivatedAbility(new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect( + Ability ability = new SimpleActivatedAbility(new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect( new TargetCardInLibrary(0, 2, filter)), new GenericManaCost(2)); ability.addCost(new TapSourceCost()); ability.addCost(new SacrificeSourceCost()); diff --git a/Mage.Sets/src/mage/cards/n/NissasPilgrimage.java b/Mage.Sets/src/mage/cards/n/NissasPilgrimage.java index 1938a494e79..a1bdc5302ca 100644 --- a/Mage.Sets/src/mage/cards/n/NissasPilgrimage.java +++ b/Mage.Sets/src/mage/cards/n/NissasPilgrimage.java @@ -4,7 +4,7 @@ package mage.cards.n; import java.util.UUID; import mage.abilities.condition.common.SpellMasteryCondition; import mage.abilities.decorator.ConditionalOneShotEffect; -import mage.abilities.effects.common.search.SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -32,8 +32,8 @@ public final class NissasPilgrimage extends CardImpl { // Search your library for up to two basic Forest cards, reveal those cards, and put one onto the battlefield tapped and the rest into your hand. Then shuffle. // Spell Mastery — If there are two or more instant and/or sorcery cards in your graveyard, search your library for up to three basic Forest cards instead of two. this.getSpellAbility().addEffect(new ConditionalOneShotEffect( - new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(new TargetCardInLibrary(0, 3, filter)), - new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(new TargetCardInLibrary(0, 2, filter)), + new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(new TargetCardInLibrary(0, 3, filter)), + new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(new TargetCardInLibrary(0, 2, filter)), SpellMasteryCondition.instance, "Search your library for up to two basic Forest cards, reveal those cards, and put one onto the battlefield tapped and the rest into your hand. Then shuffle." + "
Spell mastery — If there are two or more instant and/or sorcery cards in your graveyard, search your library for up to three basic Forest cards instead of two.")); diff --git a/Mage.Sets/src/mage/cards/p/Peregrination.java b/Mage.Sets/src/mage/cards/p/Peregrination.java index 8795a057aa0..6c28c14090d 100644 --- a/Mage.Sets/src/mage/cards/p/Peregrination.java +++ b/Mage.Sets/src/mage/cards/p/Peregrination.java @@ -1,7 +1,7 @@ package mage.cards.p; import mage.abilities.effects.Effect; -import mage.abilities.effects.common.search.SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect; +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; import mage.abilities.effects.keyword.ScryEffect; import mage.cards.*; import mage.constants.CardType; @@ -19,7 +19,7 @@ public final class Peregrination extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}"); // Search your library for up to two basic land cards, reveal those cards, and put one onto the battlefield tapped and the other into your hand. Shuffle, then scry 1. - this.getSpellAbility().addEffect(new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect( + this.getSpellAbility().addEffect(new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect( new TargetCardInLibrary(0, 2, StaticFilters.FILTER_CARD_BASIC_LANDS)) .setText("search your library for up to two basic land cards, reveal those cards, and put one onto the battlefield tapped and the other into your hand. Shuffle")); Effect effect = new ScryEffect(1); diff --git a/Mage.Sets/src/mage/cards/v/ViewpointSynchronization.java b/Mage.Sets/src/mage/cards/v/ViewpointSynchronization.java new file mode 100644 index 00000000000..ac35df8e58d --- /dev/null +++ b/Mage.Sets/src/mage/cards/v/ViewpointSynchronization.java @@ -0,0 +1,32 @@ +package mage.cards.v; + +import mage.abilities.effects.common.search.SearchLibraryPutOntoBattlefieldTappedRestInHandEffect; +import mage.abilities.keyword.FreerunningAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.filter.StaticFilters; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +public class ViewpointSynchronization extends CardImpl { + public ViewpointSynchronization(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{4}{G}"); + + this.addAbility(new FreerunningAbility("{2}{G}")); + + // Search your library for up to three basic land cards and reveal them. Put two of them onto the battlefield tapped and the other in your hand, then shuffle. + this.getSpellAbility().addEffect(new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect( + new TargetCardInLibrary(0, 3, StaticFilters.FILTER_CARD_BASIC_LANDS), 2)); + } + + public ViewpointSynchronization(ViewpointSynchronization card) { + super(card); + } + + @Override + public ViewpointSynchronization copy() { + return new ViewpointSynchronization(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AssassinsCreed.java b/Mage.Sets/src/mage/sets/AssassinsCreed.java index 2dcb67a87d1..f4eb6fffbea 100644 --- a/Mage.Sets/src/mage/sets/AssassinsCreed.java +++ b/Mage.Sets/src/mage/sets/AssassinsCreed.java @@ -321,8 +321,8 @@ public final class AssassinsCreed extends ExpansionSet { cards.add(new SetCardInfo("Towering Viewpoint", 263, Rarity.UNCOMMON, mage.cards.t.ToweringViewpoint.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Towering Viewpoint", 77, Rarity.UNCOMMON, mage.cards.t.ToweringViewpoint.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Tranquilize", 284, Rarity.COMMON, mage.cards.t.Tranquilize.class)); - //cards.add(new SetCardInfo("Viewpoint Synchronization", 223, Rarity.UNCOMMON, mage.cards.v.ViewpointSynchronization.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Viewpoint Synchronization", 43, Rarity.UNCOMMON, mage.cards.v.ViewpointSynchronization.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Viewpoint Synchronization", 223, Rarity.UNCOMMON, mage.cards.v.ViewpointSynchronization.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Viewpoint Synchronization", 43, Rarity.UNCOMMON, mage.cards.v.ViewpointSynchronization.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Waterlogged Grove", 116, Rarity.RARE, mage.cards.w.WaterloggedGrove.class)); //cards.add(new SetCardInfo("What Must Be Done", 11, Rarity.RARE, mage.cards.w.WhatMustBeDone.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("What Must Be Done", 157, Rarity.RARE, mage.cards.w.WhatMustBeDone.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect.java b/Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryPutOntoBattlefieldTappedRestInHandEffect.java similarity index 53% rename from Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect.java rename to Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryPutOntoBattlefieldTappedRestInHandEffect.java index ab7e06d9e6e..3788fb5c54b 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/search/SearchLibraryPutOntoBattlefieldTappedRestInHandEffect.java @@ -12,27 +12,37 @@ import mage.filter.FilterCard; import mage.game.Game; import mage.players.Player; import mage.target.common.TargetCardInLibrary; +import mage.util.CardUtil; /** * @author BetaSteward_at_googlemail.com, edited by Cguy7777 */ -public class SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect extends SearchEffect { +public class SearchLibraryPutOntoBattlefieldTappedRestInHandEffect extends SearchEffect { - private static final FilterCard filter = new FilterCard("card to put on the battlefield tapped"); + private final FilterCard filter; + private final int numToBattlefield; - public SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(TargetCardInLibrary target) { + public SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(TargetCardInLibrary target, int numToBattlefield) { super(target, Outcome.PutLandInPlay); staticText = "search your library for " + target.getDescription() + - ", reveal those cards, put one onto the battlefield tapped and the other into your hand, then shuffle"; + ", reveal those cards, put " + CardUtil.numberToText(numToBattlefield) + " onto the battlefield tapped and the other into your hand, then shuffle"; + this.filter = new FilterCard((numToBattlefield > 1 ? "cards" : "card") + "to put on the battlefield tapped"); + this.numToBattlefield = numToBattlefield; } - protected SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(final SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect effect) { + public SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(TargetCardInLibrary target) { + this(target, 1); + } + + protected SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(final SearchLibraryPutOntoBattlefieldTappedRestInHandEffect effect) { super(effect); + this.filter = effect.filter.copy(); + this.numToBattlefield = effect.numToBattlefield; } @Override - public SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect copy() { - return new SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect(this); + public SearchLibraryPutOntoBattlefieldTappedRestInHandEffect copy() { + return new SearchLibraryPutOntoBattlefieldTappedRestInHandEffect(this); } @Override @@ -49,14 +59,15 @@ public class SearchLibraryPutOneOntoBattlefieldTappedRestInHandEffect extends Se controller.revealCards(sourceObject.getIdName(), revealed, game); if (target.getTargets().size() >= 2) { - TargetCardInLibrary targetCardToBattlefield = new TargetCardInLibrary(filter); - controller.choose(Outcome.PutLandInPlay, revealed, targetCardToBattlefield, source, game); + int maxToBattlefield = Math.min(numToBattlefield, target.getTargets().size()); + TargetCardInLibrary targetCardsToBattlefield = new TargetCardInLibrary(maxToBattlefield, filter); + controller.choose(Outcome.PutLandInPlay, revealed, targetCardsToBattlefield, source, game); - Card cardToBattlefield = revealed.get(targetCardToBattlefield.getFirstTarget(), game); + Cards cardsToBattlefield = new CardsImpl(targetCardsToBattlefield.getTargets()); Cards cardsToHand = new CardsImpl(revealed); - if (cardToBattlefield != null) { - controller.moveCards(cardToBattlefield, Zone.BATTLEFIELD, source, game, true, false, false, null); - cardsToHand.remove(cardToBattlefield); + if (!cardsToBattlefield.isEmpty()) { + controller.moveCards(cardsToBattlefield.getCards(game), Zone.BATTLEFIELD, source, game, true, false, false, null); + cardsToHand.removeAll(cardsToBattlefield); } controller.moveCardsToHandWithInfo(cardsToHand, source, game, true); From ecbab0da9b10722edb06d579eb234da30c95e4df Mon Sep 17 00:00:00 2001 From: 4825764518 <100122841+4825764518@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:20:04 -0500 Subject: [PATCH 10/69] [PIP] Implement Strong, the Brutish Thespian (#13359) --- .../cards/s/StrongTheBrutishThespian.java | 78 +++++++++++++++++++ Mage.Sets/src/mage/sets/Fallout.java | 8 +- .../game/command/emblems/RadiationEmblem.java | 10 ++- .../main/java/mage/game/events/GameEvent.java | 3 + 4 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/s/StrongTheBrutishThespian.java diff --git a/Mage.Sets/src/mage/cards/s/StrongTheBrutishThespian.java b/Mage.Sets/src/mage/cards/s/StrongTheBrutishThespian.java new file mode 100644 index 00000000000..d7c9f8849ad --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StrongTheBrutishThespian.java @@ -0,0 +1,78 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DealtDamageToSourceTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ContinuousRuleModifyingEffectImpl; +import mage.abilities.effects.common.counter.AddCountersPlayersEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.WardAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.UUID; + +public class StrongTheBrutishThespian extends CardImpl { + public StrongTheBrutishThespian(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{G}{G}"); + this.addSuperType(SuperType.LEGENDARY); + this.addSubType(SubType.MUTANT); + this.addSubType(SubType.BERSERKER); + + this.power = new MageInt(7); + this.toughness = new MageInt(7); + + // Ward {2} + this.addAbility(new WardAbility(new ManaCostsImpl<>("{2}"))); + + // Enrage - Whenever strong is dealt damage, you get three rad counters and put three +1/+1 counters on Strong. + Ability enrageAbility = new DealtDamageToSourceTriggeredAbility(new AddCountersPlayersEffect(CounterType.RAD.createInstance(3), TargetController.YOU), false, true); + enrageAbility.addEffect(new AddCountersSourceEffect(CounterType.P1P1.createInstance(3)).setText("put three +1/+1 counters on Strong")); + this.addAbility(enrageAbility); + + // You gain life rather than lose life from radiation. + Ability healAbility = new SimpleStaticAbility(new StrongTheBrutishThespianHealEffect().setText("You gain life rather than lose life from radiation.")); + this.addAbility(healAbility); + } + + public StrongTheBrutishThespian(StrongTheBrutishThespian card) { + super(card); + } + + @Override + public StrongTheBrutishThespian copy() { + return new StrongTheBrutishThespian(this); + } + + class StrongTheBrutishThespianHealEffect extends ContinuousRuleModifyingEffectImpl { + + protected StrongTheBrutishThespianHealEffect() { + super(Duration.Custom, Outcome.Benefit); + } + + public StrongTheBrutishThespianHealEffect(StrongTheBrutishThespianHealEffect effect) { + super(effect); + } + + @Override + public StrongTheBrutishThespianHealEffect copy() { + return new StrongTheBrutishThespianHealEffect(this); + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.RADIATION_GAIN_LIFE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return event.getPlayerId().equals(source.getControllerId()); + } + } +} diff --git a/Mage.Sets/src/mage/sets/Fallout.java b/Mage.Sets/src/mage/sets/Fallout.java index 8a977e570c8..cb256341aae 100644 --- a/Mage.Sets/src/mage/sets/Fallout.java +++ b/Mage.Sets/src/mage/sets/Fallout.java @@ -835,10 +835,10 @@ public final class Fallout extends ExpansionSet { //cards.add(new SetCardInfo("Strong Back", 611, Rarity.RARE, mage.cards.s.StrongBack.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Strong Back", 83, Rarity.RARE, mage.cards.s.StrongBack.class, NON_FULL_USE_VARIOUS)); //cards.add(new SetCardInfo("Strong Back", 930, Rarity.RARE, mage.cards.s.StrongBack.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 403, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 612, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 84, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Strong, the Brutish Thespian", 931, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Strong, the Brutish Thespian", 403, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Strong, the Brutish Thespian", 612, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Strong, the Brutish Thespian", 84, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Strong, the Brutish Thespian", 931, Rarity.RARE, mage.cards.s.StrongTheBrutishThespian.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Struggle for Project Purity", 380, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Struggle for Project Purity", 39, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Struggle for Project Purity", 567, Rarity.RARE, mage.cards.s.StruggleForProjectPurity.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage/src/main/java/mage/game/command/emblems/RadiationEmblem.java b/Mage/src/main/java/mage/game/command/emblems/RadiationEmblem.java index 2593c8a2d74..9c2fcd54a1a 100644 --- a/Mage/src/main/java/mage/game/command/emblems/RadiationEmblem.java +++ b/Mage/src/main/java/mage/game/command/emblems/RadiationEmblem.java @@ -16,6 +16,7 @@ import mage.counters.CounterType; import mage.filter.StaticFilters; import mage.game.Game; import mage.game.command.Emblem; +import mage.game.events.GameEvent; import mage.players.Player; /** @@ -106,8 +107,13 @@ class RadiationEffect extends OneShotEffect { Cards milled = player.millCards(amount, source, game); int countNonLand = milled.count(StaticFilters.FILTER_CARD_NON_LAND, player.getId(), source, game); if (countNonLand > 0) { - // TODO: support gaining life instead with [[Strong, the Brutish Thespian]] - player.loseLife(countNonLand, game, source, false); + GameEvent event = new GameEvent(GameEvent.EventType.RADIATION_GAIN_LIFE, null, source, player.getId(), amount, false); + if (game.replaceEvent(event)) { + player.gainLife(countNonLand, game, source); + } else { + player.loseLife(countNonLand, game, source, false); + } + player.loseCounters(CounterType.RAD.getName(), countNonLand, source, game); } return true; diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 73d985e9493..77f1fd0ea22 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -672,6 +672,9 @@ public class GameEvent implements Serializable { playerId player who gave the gift */ GAVE_GIFT, + /* rad counter life loss/gain effect + */ + RADIATION_GAIN_LIFE, // custom events - must store some unique data to track CUSTOM_EVENT; From f5d4c957fc621e6bf016bd4ac1565e7abc753419 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 14 Feb 2025 12:50:36 -0500 Subject: [PATCH 11/69] [DFT] small update to max speed mechanic to match comp rules --- .../main/java/mage/abilities/common/MaxSpeedAbility.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java b/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java index d97c23e9c16..7f308bb05e2 100644 --- a/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java +++ b/Mage/src/main/java/mage/abilities/common/MaxSpeedAbility.java @@ -7,7 +7,10 @@ import mage.abilities.dynamicvalue.common.ControllerSpeedCount; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.Effect; import mage.cards.Card; -import mage.constants.*; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; import mage.game.Game; import mage.game.permanent.Permanent; import mage.util.CardUtil; @@ -22,7 +25,7 @@ public class MaxSpeedAbility extends StaticAbility { } public MaxSpeedAbility(Ability ability) { - super(Zone.ALL, new MaxSpeedAbilityEffect(ability)); + super(ability.getZone(), new MaxSpeedAbilityEffect(ability)); } private MaxSpeedAbility(final MaxSpeedAbility ability) { From 5eec4c4465f8acbee9b3ef8511490f6b9bd44502 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 18 Feb 2025 09:53:16 -0500 Subject: [PATCH 12/69] [FIC] add set --- .../dl/sources/ScryfallImageSupportCards.java | 1 + .../src/mage/sets/FinalFantasyCommander.java | 21 +++++++++++++++++++ Utils/known-sets.txt | 1 + Utils/mtg-cards-data.txt | 4 ++++ Utils/mtg-sets-data.txt | 1 + 5 files changed, 28 insertions(+) create mode 100644 Mage.Sets/src/mage/sets/FinalFantasyCommander.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java index f7f4f6ca7d1..4e090af712b 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java @@ -579,6 +579,7 @@ public class ScryfallImageSupportCards { add("INR"); // Innistrad Remastered add("DFT"); // Aetherdrift add("DRC"); // Aetherdrift Commander + add("FIC"); // Final Fantasy Commander // Custom sets using Scryfall images - must provide a direct link for each card in directDownloadLinks add("CALC"); // Custom Alchemized versions of existing cards diff --git a/Mage.Sets/src/mage/sets/FinalFantasyCommander.java b/Mage.Sets/src/mage/sets/FinalFantasyCommander.java new file mode 100644 index 00000000000..135ab35a02f --- /dev/null +++ b/Mage.Sets/src/mage/sets/FinalFantasyCommander.java @@ -0,0 +1,21 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.SetType; + +/** + * @author TheElk801 + */ +public final class FinalFantasyCommander extends ExpansionSet { + + private static final FinalFantasyCommander instance = new FinalFantasyCommander(); + + public static FinalFantasyCommander getInstance() { + return instance; + } + + private FinalFantasyCommander() { + super("Final Fantasy Commander", "FIC", ExpansionSet.buildDate(2025, 6, 13), SetType.SUPPLEMENTAL); + this.hasBasicLands = false; + } +} diff --git a/Utils/known-sets.txt b/Utils/known-sets.txt index 4cf1378a4f3..e0a2996f7ad 100644 --- a/Utils/known-sets.txt +++ b/Utils/known-sets.txt @@ -98,6 +98,7 @@ Fallen Empires|FallenEmpires| Fate Reforged|FateReforged| Fifth Dawn|FifthDawn| Fifth Edition|FifthEdition| +Final Fantasy Commander|FinalFantasyCommander| Foundations|Foundations| Foundations Jumpstart|FoundationsJumpstart| Fourth Edition|FourthEdition| diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index db57fb79528..09e46efa29b 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -57177,3 +57177,7 @@ Underground River|Aetherdrift Commander|181|R||Land|||{T}: Add {C}.${T}: Add {U} Unholy Grotto|Aetherdrift Commander|182|R||Land|||{T}: Add {C}.${B}, {T}: Put target Zombie card from your graveyard on top of your library.| Vineglimmer Snarl|Aetherdrift Commander|183|R||Land|||As Vineglimmer Snarl enters, you may reveal a Forest or Island card from your hand. If you don't, Vineglimmer Snarl enters tapped.${T}: Add {G} or {U}.| Yavimaya Coast|Aetherdrift Commander|184|R||Land|||{T}: Add {C}.${T}: Add {G} or {U}. Yavimaya Coast deals 1 damage to you.| +Cloud, Ex-SOLDIER|Final Fantasy Commander|2|M|{2}{R}{G}{W}|Legendary Creature - Human Soldier Mercenary|4|4|Haste$When Cloud enters, attach up to one target Equipment you control to it.$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.| +Terra, Herald of Hope|Final Fantasy Commander|4|M|{R}{W}{B}|Legendary Creature - Human Wizard Warrior|3|3|Trance -- At the beginning of combat on your turn, mill two cards. Terra gains flying until end of turn.$Whenever Terra deals combat damage to a player, you may pay {2}. When you do, return target creature card with power 3 or less from your graveyard to the battlefield tapped.| +Tidus, Yuna's Guardian|Final Fantasy Commander|5|M|{G}{W}{U}|Legendary Creature - Human Warrior|3|3|At the beginning of combat on your turn, you may move a counter from target creature you control onto a second target creature you control.$Cheer - Whenever one or more creatures you control with counters on them deal combat damage to a player, you may draw a card and proliferate. Do this only once each turn.| +Y'shtola, Night's Blessed|Final Fantasy Commander|7|M|{1}{W}{U}{B}|Legendary Creature - Cat Warlock|2|4|Vigilance$At the beginning of each end step, if a player lost 4 or more life this turn, you draw a card.$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.| diff --git a/Utils/mtg-sets-data.txt b/Utils/mtg-sets-data.txt index fe89cf9a9a3..ce764384d19 100644 --- a/Utils/mtg-sets-data.txt +++ b/Utils/mtg-sets-data.txt @@ -111,6 +111,7 @@ Exodus|EXO| Zendikar Expeditions|EXP| Fallout|PIP| Fallen Empires|FEM| +Final Fantasy Commander|FIC| Friday Night Magic|FNMP| Fate Reforged|FRF| Foundations|FDN| From fe8f54a64c45122818ecdd795f20b2aa52fac769 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 18 Feb 2025 10:09:40 -0500 Subject: [PATCH 13/69] [FIC] Implement Terra, Herald of Hope --- .../src/mage/cards/t/TerraHeraldOfHope.java | 68 +++++++++++++++++++ .../src/mage/sets/FinalFantasyCommander.java | 3 + 2 files changed, 71 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java diff --git a/Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java b/Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java new file mode 100644 index 00000000000..4ee0be2a5ad --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TerraHeraldOfHope.java @@ -0,0 +1,68 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.common.DoWhenCostPaid; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TerraHeraldOfHope extends CardImpl { + + private static final FilterCard filter = new FilterCreatureCard("creature card with power 3 or less from your graveyard"); + + static { + filter.add(new PowerPredicate(ComparisonType.FEWER_THAN, 4)); + } + + public TerraHeraldOfHope(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}{W}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WIZARD); + this.subtype.add(SubType.WARRIOR); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Trance -- At the beginning of combat on your turn, mill two cards. Terra gains flying until end of turn. + Ability ability = new BeginningOfCombatTriggeredAbility(new MillCardsControllerEffect(2)); + ability.addEffect(new GainAbilitySourceEffect(FlyingAbility.getInstance(), Duration.EndOfTurn)); + this.addAbility(ability.withFlavorWord("Trance")); + + // Whenever Terra deals combat damage to a player, you may pay {2}. When you do, return target creature card with power 3 or less from your graveyard to the battlefield tapped. + ReflexiveTriggeredAbility reflexiveAbility = new ReflexiveTriggeredAbility( + new ReturnFromGraveyardToBattlefieldTargetEffect(true), false + ); + ability.addTarget(new TargetCardInYourGraveyard(filter)); + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( + new DoWhenCostPaid(reflexiveAbility, new GenericManaCost(2), "Pay {2}?") + )); + } + + private TerraHeraldOfHope(final TerraHeraldOfHope card) { + super(card); + } + + @Override + public TerraHeraldOfHope copy() { + return new TerraHeraldOfHope(this); + } +} diff --git a/Mage.Sets/src/mage/sets/FinalFantasyCommander.java b/Mage.Sets/src/mage/sets/FinalFantasyCommander.java index 135ab35a02f..2ab48df6768 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasyCommander.java +++ b/Mage.Sets/src/mage/sets/FinalFantasyCommander.java @@ -1,6 +1,7 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.constants.Rarity; import mage.constants.SetType; /** @@ -17,5 +18,7 @@ public final class FinalFantasyCommander extends ExpansionSet { private FinalFantasyCommander() { super("Final Fantasy Commander", "FIC", ExpansionSet.buildDate(2025, 6, 13), SetType.SUPPLEMENTAL); this.hasBasicLands = false; + + cards.add(new SetCardInfo("Terra, Herald of Hope", 4, Rarity.MYTHIC, mage.cards.t.TerraHeraldOfHope.class)); } } From 33b3f53643be6ea1924085f2b93e80af9d584d74 Mon Sep 17 00:00:00 2001 From: padfoothelix Date: Tue, 18 Feb 2025 16:33:13 +0100 Subject: [PATCH 14/69] [WHO] Implement Bill Potts (#13338) --- Mage.Sets/src/mage/cards/b/BillPotts.java | 124 ++++++++++++++++++++++ Mage.Sets/src/mage/sets/DoctorWho.java | 8 +- 2 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/b/BillPotts.java diff --git a/Mage.Sets/src/mage/cards/b/BillPotts.java b/Mage.Sets/src/mage/cards/b/BillPotts.java new file mode 100644 index 00000000000..ad9402aa5a0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BillPotts.java @@ -0,0 +1,124 @@ +package mage.cards.b; + +import mage.MageInt; +import mage.MageItem; +import mage.abilities.Ability; +import mage.abilities.Abilities; +import mage.abilities.AbilitiesImpl; +import mage.abilities.Mode; +import mage.abilities.common.ActivateAbilityTriggeredAbility; +import mage.abilities.common.SpellCastControllerTriggeredAbility; +import mage.abilities.effects.common.CopyStackObjectEffect; +import mage.abilities.meta.OrTriggeredAbility; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.abilities.keyword.DoctorsCompanionAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SetTargetPointer; +import mage.constants.Zone; +import mage.filter.common.FilterInstantOrSorcerySpell; +import mage.filter.FilterSpell; +import mage.filter.FilterStackObject; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.game.Game; +import mage.game.stack.StackObject; +import mage.game.stack.Spell; +import mage.target.Target; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.UUID; + +/** + * + * @author padfoot + */ +public final class BillPotts extends CardImpl { + + private static final FilterSpell filterInstantOrSorcery = new FilterInstantOrSorcerySpell("an instant or sorcery that targets only {this}"); + private static final FilterStackObject filterAbility = new FilterStackObject("an ability that targets only {this}"); + + static { + filterInstantOrSorcery.add(BillPottsPredicate.instance); + filterAbility.add(BillPottsPredicate.instance); + } + + public BillPotts(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Whenever you cast an instant or sorcery spell that targets only Bill Potts or activate an ability that targets only Bill Potts, copy that spell or ability. You may choose new targets for the copy. This ability triggers only once each turn. + this.addAbility(new OrTriggeredAbility( + Zone.BATTLEFIELD, + new CopyStackObjectEffect("that spell or ability"), + false, + "", + new SpellCastControllerTriggeredAbility( + null, + filterInstantOrSorcery, + false, + SetTargetPointer.SPELL + ), + new ActivateAbilityTriggeredAbility( + null, + filterAbility, + SetTargetPointer.SPELL + ) + ).setTriggersLimitEachTurn(1)); + + // Doctor's companion + this.addAbility(DoctorsCompanionAbility.getInstance()); + + } + + private BillPotts(final BillPotts card) { + super(card); + } + + @Override + public BillPotts copy() { + return new BillPotts(this); + } +} + +enum BillPottsPredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + List oneTargetList = Arrays.asList(input.getSourceId()); + return (makeStream(input, game).collect(Collectors.toList()).equals(oneTargetList)); + } + + private static final Stream makeStream(ObjectSourcePlayer input, Game game) { + Abilities objectAbilities = new AbilitiesImpl<>(); + if (input.getObject() instanceof Spell) { + objectAbilities.addAll(((Spell) input.getObject()).getSpellAbilities()); + } else { + objectAbilities.add(input.getObject().getStackAbility()); + } + return objectAbilities + .stream() + .map(Ability::getModes) + .flatMap(m -> m.getSelectedModes().stream().map(m::get)) + .filter(Objects::nonNull) + .map(Mode::getTargets) + .flatMap(Collection::stream) + .filter(t -> !t.isNotTarget()) + .map(Target::getTargets) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .distinct(); + } +} diff --git a/Mage.Sets/src/mage/sets/DoctorWho.java b/Mage.Sets/src/mage/sets/DoctorWho.java index e0a8dc21424..22e233305a0 100644 --- a/Mage.Sets/src/mage/sets/DoctorWho.java +++ b/Mage.Sets/src/mage/sets/DoctorWho.java @@ -95,10 +95,10 @@ public final class DoctorWho extends ExpansionSet { cards.add(new SetCardInfo("Bessie, the Doctor's Roadster", 776, Rarity.RARE, mage.cards.b.BessieTheDoctorsRoadster.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Bigger on the Inside", 115, Rarity.UNCOMMON, mage.cards.b.BiggerOnTheInside.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Bigger on the Inside", 720, Rarity.UNCOMMON, mage.cards.b.BiggerOnTheInside.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Bill Potts", 379, Rarity.RARE, mage.cards.b.BillPotts.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Bill Potts", 681, Rarity.RARE, mage.cards.b.BillPotts.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Bill Potts", 76, Rarity.RARE, mage.cards.b.BillPotts.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Bill Potts", 970, Rarity.RARE, mage.cards.b.BillPotts.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Bill Potts", 379, Rarity.RARE, mage.cards.b.BillPotts.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Bill Potts", 681, Rarity.RARE, mage.cards.b.BillPotts.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Bill Potts", 76, Rarity.RARE, mage.cards.b.BillPotts.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Bill Potts", 970, Rarity.RARE, mage.cards.b.BillPotts.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Blasphemous Act", 1063, Rarity.RARE, mage.cards.b.BlasphemousAct.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Blasphemous Act", 224, Rarity.RARE, mage.cards.b.BlasphemousAct.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Blasphemous Act", 472, Rarity.RARE, mage.cards.b.BlasphemousAct.class, NON_FULL_USE_VARIOUS)); From 27ff3ee874a1d671ac9b6fb7fe54d23536441dc8 Mon Sep 17 00:00:00 2001 From: 4825764518 <100122841+4825764518@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:52:06 -0500 Subject: [PATCH 15/69] [ACR] Implement Brotherhood Headquarters (#13360) --- .../mage/cards/b/BrotherhoodHeadquarters.java | 106 ++++++++++++++++++ Mage.Sets/src/mage/sets/AssassinsCreed.java | 4 +- 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/b/BrotherhoodHeadquarters.java diff --git a/Mage.Sets/src/mage/cards/b/BrotherhoodHeadquarters.java b/Mage.Sets/src/mage/cards/b/BrotherhoodHeadquarters.java new file mode 100644 index 00000000000..6080eb9c937 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BrotherhoodHeadquarters.java @@ -0,0 +1,106 @@ +package mage.cards.b; + +import mage.ConditionalMana; +import mage.MageObject; +import mage.Mana; +import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.condition.Condition; +import mage.abilities.keyword.FreerunningAbility; +import mage.abilities.mana.ColorlessManaAbility; +import mage.abilities.mana.ConditionalAnyColorManaAbility; +import mage.abilities.mana.builder.ConditionalManaBuilder; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.Filter; +import mage.filter.predicate.Predicate; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.Game; + +import java.util.UUID; + +public class BrotherhoodHeadquarters extends CardImpl { + + private static final Predicate predicate = new AbilityPredicate(FreerunningAbility.class); + + public BrotherhoodHeadquarters(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.LAND}, ""); + + // Add one colorless + this.addAbility(new ColorlessManaAbility()); + + // Add one mana of any color. Spend this mana only to cast an Assassin spell or a spell that has freerunning, or to activate an ability of an Assassin source. + this.addAbility(new ConditionalAnyColorManaAbility(1, new BrotherhoodHeadquartersManaBuilder())); + } + + public BrotherhoodHeadquarters(BrotherhoodHeadquarters card) { + super(card); + } + + @Override + public BrotherhoodHeadquarters copy() { + return new BrotherhoodHeadquarters(this); + } + + class BrotherhoodHeadquartersManaBuilder extends ConditionalManaBuilder { + + @Override + public String getRule() { + return "Spend this mana only to cast an Assassin spell or a spell that has freerunning, or to activate an ability of an Assassin source."; + } + + @Override + public ConditionalMana build(Object... options) { + return new BrotherhoodHeadquartersConditionalMana(this.mana); + } + } + + class BrotherhoodHeadquartersConditionalMana extends ConditionalMana { + + public BrotherhoodHeadquartersConditionalMana(Mana mana) { + super(mana); + addCondition(new BrotherhoodHeadquartersAssassinSpellManaCondition()); + addCondition(new BrotherhoodHeadquartersFreerunningManaCondition()); + addCondition(new BrotherhoodHeadquartersAssassinSourceManaCondition()); + setComparisonScope(Filter.ComparisonScope.Any); + } + } + + class BrotherhoodHeadquartersAssassinSpellManaCondition implements Condition { + + @Override + public boolean apply(Game game, Ability source) { + if (source instanceof SpellAbility) { + Card card = game.getCard(source.getSourceId()); + return card != null && card.hasSubtype(SubType.ASSASSIN, game); + } + + return false; + } + } + + class BrotherhoodHeadquartersFreerunningManaCondition implements Condition { + + @Override + public boolean apply(Game game, Ability source) { + MageObject sourceObject = game.getObject(source); + return sourceObject != null && predicate.apply(sourceObject, game); + } + } + + class BrotherhoodHeadquartersAssassinSourceManaCondition implements Condition { + + @Override + public boolean apply(Game game, Ability source) { + if (source.isActivatedAbility()) { + MageObject object = game.getObject(source.getSourceId()); + return object != null && object.hasSubtype(SubType.ASSASSIN, game); + } + + return false; + } + } +} diff --git a/Mage.Sets/src/mage/sets/AssassinsCreed.java b/Mage.Sets/src/mage/sets/AssassinsCreed.java index f4eb6fffbea..3ffa041e3a9 100644 --- a/Mage.Sets/src/mage/sets/AssassinsCreed.java +++ b/Mage.Sets/src/mage/sets/AssassinsCreed.java @@ -77,8 +77,8 @@ public final class AssassinsCreed extends ExpansionSet { cards.add(new SetCardInfo("Bleeding Effect", 232, Rarity.UNCOMMON, mage.cards.b.BleedingEffect.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Bleeding Effect", 51, Rarity.UNCOMMON, mage.cards.b.BleedingEffect.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Brotherhood Ambushers", 285, Rarity.UNCOMMON, mage.cards.b.BrotherhoodAmbushers.class)); - //cards.add(new SetCardInfo("Brotherhood Headquarters", 266, Rarity.UNCOMMON, mage.cards.b.BrotherhoodHeadquarters.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Brotherhood Headquarters", 80, Rarity.UNCOMMON, mage.cards.b.BrotherhoodHeadquarters.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Brotherhood Headquarters", 266, Rarity.UNCOMMON, mage.cards.b.BrotherhoodHeadquarters.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Brotherhood Headquarters", 80, Rarity.UNCOMMON, mage.cards.b.BrotherhoodHeadquarters.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Brotherhood Patriarch", 286, Rarity.COMMON, mage.cards.b.BrotherhoodPatriarch.class)); cards.add(new SetCardInfo("Brotherhood Regalia", 255, Rarity.UNCOMMON, mage.cards.b.BrotherhoodRegalia.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Brotherhood Regalia", 71, Rarity.UNCOMMON, mage.cards.b.BrotherhoodRegalia.class, NON_FULL_USE_VARIOUS)); From 2174797c4b2fdc1982f3eac1c0a67902e2149f25 Mon Sep 17 00:00:00 2001 From: jimga150 Date: Tue, 18 Feb 2025 10:57:13 -0500 Subject: [PATCH 16/69] Mu Yanling, Wind Rider - fixed not working second ability (#13361) --- .../src/mage/cards/m/MuYanlingWindRider.java | 2 +- .../single/dft/MuYanlingWindRiderTest.java | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/dft/MuYanlingWindRiderTest.java diff --git a/Mage.Sets/src/mage/cards/m/MuYanlingWindRider.java b/Mage.Sets/src/mage/cards/m/MuYanlingWindRider.java index bd9badc3070..d2d4b3cdd3a 100644 --- a/Mage.Sets/src/mage/cards/m/MuYanlingWindRider.java +++ b/Mage.Sets/src/mage/cards/m/MuYanlingWindRider.java @@ -32,7 +32,7 @@ public final class MuYanlingWindRider extends CardImpl { = new FilterCreaturePermanent("creatures you control with flying"); static { - filter.add(new AbilityPredicate(FlyingAbility.class)); + filter2.add(new AbilityPredicate(FlyingAbility.class)); } public MuYanlingWindRider(UUID ownerId, CardSetInfo setInfo) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/MuYanlingWindRiderTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/MuYanlingWindRiderTest.java new file mode 100644 index 00000000000..882c97fdb57 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/MuYanlingWindRiderTest.java @@ -0,0 +1,63 @@ +package org.mage.test.cards.single.dft; + +import mage.abilities.keyword.FlyingAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author jimga150 + */ +public class MuYanlingWindRiderTest extends CardTestPlayerBase { + + // When this creature enters, create a 3/2 colorless Vehicle artifact token with crew 1. + // Vehicles you control have flying. + // Whenever one or more creatures you control with flying deal combat damage to a player, draw a card. + private final String muyanling = "Mu Yanling, Wind Rider"; + + @Test + public void testToken() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + addCard(Zone.HAND, playerA, muyanling); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, muyanling, true); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertPermanentCount(playerA, muyanling, 1); + assertPermanentCount(playerA, "Vehicle Token", 1); + assertAbility(playerA, "Vehicle Token", FlyingAbility.getInstance(), true); + } + + @Test + public void testDraw() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + addCard(Zone.HAND, playerA, muyanling); + addCard(Zone.HAND, playerA, "Memnite"); + addCard(Zone.BATTLEFIELD, playerA, "Ankle Biter"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, muyanling, true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite", true); + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Crew 1"); + setChoice(playerA, "Memnite"); + + attack(3, playerA, "Vehicle Token", playerB); + attack(3, playerA, muyanling, playerB); + attack(3, playerA, "Ankle Biter", playerB); + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + setStrictChooseMode(true); + execute(); + + assertPermanentCount(playerA, muyanling, 1); + assertPermanentCount(playerA, "Vehicle Token", 1); + assertAbility(playerA, "Vehicle Token", FlyingAbility.getInstance(), true); + assertHandCount(playerA, 2); // 1 for turn plus 1 for attack + } + +} From dc7b5cd84fbe0c497a7c57689406bf05037738f2 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 18 Feb 2025 14:31:50 -0500 Subject: [PATCH 17/69] [FIN] add set --- .../dl/sources/ScryfallImageSupportCards.java | 1 + Mage.Sets/src/mage/sets/FinalFantasy.java | 21 +++++++++++++++++++ Utils/known-sets.txt | 1 + Utils/mtg-cards-data.txt | 16 ++++++++++++++ Utils/mtg-sets-data.txt | 1 + 5 files changed, 40 insertions(+) create mode 100644 Mage.Sets/src/mage/sets/FinalFantasy.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java index 4e090af712b..6b4a44037da 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java @@ -579,6 +579,7 @@ public class ScryfallImageSupportCards { add("INR"); // Innistrad Remastered add("DFT"); // Aetherdrift add("DRC"); // Aetherdrift Commander + add("FIN"); // Final Fantasy add("FIC"); // Final Fantasy Commander // Custom sets using Scryfall images - must provide a direct link for each card in directDownloadLinks diff --git a/Mage.Sets/src/mage/sets/FinalFantasy.java b/Mage.Sets/src/mage/sets/FinalFantasy.java new file mode 100644 index 00000000000..1b7bc666676 --- /dev/null +++ b/Mage.Sets/src/mage/sets/FinalFantasy.java @@ -0,0 +1,21 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.SetType; + +/** + * @author TheElk801 + */ +public final class FinalFantasy extends ExpansionSet { + + private static final FinalFantasy instance = new FinalFantasy(); + + public static FinalFantasy getInstance() { + return instance; + } + + private FinalFantasy() { + super("Final Fantasty", "FIN", ExpansionSet.buildDate(2025, 6, 13), SetType.EXPANSION); + this.blockName = "Final Fantasty"; // for sorting in GUI + } +} diff --git a/Utils/known-sets.txt b/Utils/known-sets.txt index e0a2996f7ad..daa56f04537 100644 --- a/Utils/known-sets.txt +++ b/Utils/known-sets.txt @@ -98,6 +98,7 @@ Fallen Empires|FallenEmpires| Fate Reforged|FateReforged| Fifth Dawn|FifthDawn| Fifth Edition|FifthEdition| +Final Fantasy|FinalFantasy| Final Fantasy Commander|FinalFantasyCommander| Foundations|Foundations| Foundations Jumpstart|FoundationsJumpstart| diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 09e46efa29b..56dfafab680 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -57181,3 +57181,19 @@ Cloud, Ex-SOLDIER|Final Fantasy Commander|2|M|{2}{R}{G}{W}|Legendary Creature - Terra, Herald of Hope|Final Fantasy Commander|4|M|{R}{W}{B}|Legendary Creature - Human Wizard Warrior|3|3|Trance -- At the beginning of combat on your turn, mill two cards. Terra gains flying until end of turn.$Whenever Terra deals combat damage to a player, you may pay {2}. When you do, return target creature card with power 3 or less from your graveyard to the battlefield tapped.| Tidus, Yuna's Guardian|Final Fantasy Commander|5|M|{G}{W}{U}|Legendary Creature - Human Warrior|3|3|At the beginning of combat on your turn, you may move a counter from target creature you control onto a second target creature you control.$Cheer - Whenever one or more creatures you control with counters on them deal combat damage to a player, you may draw a card and proliferate. Do this only once each turn.| Y'shtola, Night's Blessed|Final Fantasy Commander|7|M|{1}{W}{U}{B}|Legendary Creature - Cat Warlock|2|4|Vigilance$At the beginning of each end step, if a player lost 4 or more life this turn, you draw a card.$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.| +Sidequest: Catch a Fish|Final Fantasy|31|U|{2}{W}|Enchantment|||At the beginning of your upkeep, look at the top card of your libraray. If it's an aartifact or creature card, you may reveal it and put it into your hand. If you put a card into your hand this way, create a Food token and transform this enchantment.| +Cooking Campsite|Final Fantasy|31|U||Land|||{T}: Add {W}.${3}, {T}, Sacrifice an artifact: Put a +1/+1 counter on each creature you control. Activate only as a sorcery.| +Stiltzkin, Moogle Merchant|Final Fantasy|34|R|{W}|Legendary Creature Moogle|1|2|Lifelink${2}, {T}: Target opponent gains control of another target permanent you control. If they do, you draw a card.| +Summon: Shiva|Final Fantasy|78|U|{3}{U}{U}|Enchantment Creature - Saga Elemental|4|5|(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)$Heavenly Strike -- Tap target creature an opponent controls. Put a stun counter it.$Diamond Dust -- Draw a card for each tapped creature your opponents control.| +Cecil, Dark Knight|Final Fantasy|91|R|{B}|Legendary Creature - Human Knight|2|3|Deathtouch$Darkness -- Whenever Cecil deals damage, you lose that much life. Then if your life total is less than or equal to half your starting life total, untap Cecil and transform it.| +Cecil, Redeemed Paladin|Final Fantasy|91|R||Legendary Creature - Human Knight|4|4|Lifelink$Protect Whenever Cecil attacks, other attacking creatures gain indestructible until end of turn.| +Tonberry|Final Fantasy|122|U|{B}|Creature - Salamander Horror|2|1|This creature enters tapped with a stun counter on it.$Chef's Knife -- During your turn, this creature has first strike and deathtouch.| +Jumbo Cactuar|Final Fantasy|191|R|{5}{G}{G}|Creature - Plant|1|7|10,000 Needles -- Whenever this creature attacks, it gets +9999/+0 until end of turn.| +Sazh's Chocobo|Final Fantasy|200|U|{G}|Creature - Bird|0|1|Landfall -- Whenever a land you control enters, put a+1/+1 counter on this creature.| +Emet-Selch, Unsundered|Final Fantasy|218|M|{1}{U}{B}|Legendary Creature - Elder Wizard|2|4|Vigilance$Whenever Emet-Selch enters or attacks, draw a card, then discard a card.$At the beginning of your upkeep, if there are fourteen or more cards in your graveyard, you may transform Emet-Selch.| +Hades, Sorcerer of Eld|Final Fantasy|218|M||Legendary Creature - Avatar|6|6|Vigilance$Echo of the Lost -- During your turn, you may play cards from your graveyard.$If a card or token would be put into your graveyard from anywhere, exile it instead.| +Garland, Knight of Cornelia|Final Fantasy|221|U|{B}{R}|Legendary Creature - Human Knight|3|2|Whenever you cast a noncreature spell, surveil 1.${3}{B}{B}{R}{R}: Return this card from your graveyard to the battlefield transformed. Activate only as a sorcery.| +Chaos, the Endless|Final Fantasy|221|U||Legendary Creature - Demon|5|5|Flying$When Chaos dies, put it on the bottom of its owner's library.| +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.| diff --git a/Utils/mtg-sets-data.txt b/Utils/mtg-sets-data.txt index ce764384d19..95822e07c65 100644 --- a/Utils/mtg-sets-data.txt +++ b/Utils/mtg-sets-data.txt @@ -111,6 +111,7 @@ Exodus|EXO| Zendikar Expeditions|EXP| Fallout|PIP| Fallen Empires|FEM| +Final Fantasy|FIN| Final Fantasy Commander|FIC| Friday Night Magic|FNMP| Fate Reforged|FRF| From 141ec011069c1c02cf90c01ede640e59c9a88e55 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 18 Feb 2025 14:37:16 -0500 Subject: [PATCH 18/69] [FIN] Implement Sazh's Chocobo --- Mage.Sets/src/mage/cards/s/SazhsChocobo.java | 38 ++++++++++++++++++++ Mage.Sets/src/mage/sets/FinalFantasy.java | 3 ++ 2 files changed, 41 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SazhsChocobo.java diff --git a/Mage.Sets/src/mage/cards/s/SazhsChocobo.java b/Mage.Sets/src/mage/cards/s/SazhsChocobo.java new file mode 100644 index 00000000000..bb722c1b757 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SazhsChocobo.java @@ -0,0 +1,38 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.LandfallAbility; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SazhsChocobo extends CardImpl { + + public SazhsChocobo(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{G}"); + + this.subtype.add(SubType.BIRD); + this.power = new MageInt(0); + this.toughness = new MageInt(1); + + // Landfall -- Whenever a land you control enters, put a +1/+1 counter on this creature. + this.addAbility(new LandfallAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance()))); + } + + private SazhsChocobo(final SazhsChocobo card) { + super(card); + } + + @Override + public SazhsChocobo copy() { + return new SazhsChocobo(this); + } +} diff --git a/Mage.Sets/src/mage/sets/FinalFantasy.java b/Mage.Sets/src/mage/sets/FinalFantasy.java index 1b7bc666676..9128695ff88 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasy.java +++ b/Mage.Sets/src/mage/sets/FinalFantasy.java @@ -1,6 +1,7 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.constants.Rarity; import mage.constants.SetType; /** @@ -17,5 +18,7 @@ public final class FinalFantasy extends ExpansionSet { private FinalFantasy() { super("Final Fantasty", "FIN", ExpansionSet.buildDate(2025, 6, 13), SetType.EXPANSION); this.blockName = "Final Fantasty"; // for sorting in GUI + + cards.add(new SetCardInfo("Sazh's Chocobo", 200, Rarity.UNCOMMON, mage.cards.s.SazhsChocobo.class)); } } From 2ce00d160bcc9f30bbdff93b166f8dd7af61e53f Mon Sep 17 00:00:00 2001 From: theelk801 Date: Tue, 18 Feb 2025 15:29:47 -0500 Subject: [PATCH 19/69] fix verify failure --- Mage.Sets/src/mage/sets/FinalFantasy.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/sets/FinalFantasy.java b/Mage.Sets/src/mage/sets/FinalFantasy.java index 9128695ff88..e6b322b0176 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasy.java +++ b/Mage.Sets/src/mage/sets/FinalFantasy.java @@ -16,8 +16,9 @@ public final class FinalFantasy extends ExpansionSet { } private FinalFantasy() { - super("Final Fantasty", "FIN", ExpansionSet.buildDate(2025, 6, 13), SetType.EXPANSION); - this.blockName = "Final Fantasty"; // for sorting in GUI + super("Final Fantasy", "FIN", ExpansionSet.buildDate(2025, 6, 13), SetType.EXPANSION); + this.blockName = "Final Fantasy"; // for sorting in GUI + this.hasBasicLands = false; // temporary cards.add(new SetCardInfo("Sazh's Chocobo", 200, Rarity.UNCOMMON, mage.cards.s.SazhsChocobo.class)); } From 2d827c9375eae32238943046d7ffadfe452561c2 Mon Sep 17 00:00:00 2001 From: Steven Knipe Date: Thu, 20 Feb 2025 12:32:56 -0800 Subject: [PATCH 20/69] Fix Hakim Loreweaver condition --- Mage.Sets/src/mage/cards/h/HakimLoreweaver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/h/HakimLoreweaver.java b/Mage.Sets/src/mage/cards/h/HakimLoreweaver.java index a10fc2dc814..6ba435e5547 100644 --- a/Mage.Sets/src/mage/cards/h/HakimLoreweaver.java +++ b/Mage.Sets/src/mage/cards/h/HakimLoreweaver.java @@ -136,7 +136,7 @@ enum HakimLoreweaverCondition implements Condition { .stream() .map(game::getPermanent) .filter(Objects::nonNull) - .anyMatch(permanent -> permanent.hasSubtype(SubType.AURA, game)); + .noneMatch(permanent -> permanent.hasSubtype(SubType.AURA, game)); } @Override From f2272045ad6f242e19e9efd4828e8305e8ea56b2 Mon Sep 17 00:00:00 2001 From: Steven Knipe Date: Thu, 20 Feb 2025 13:47:33 -0800 Subject: [PATCH 21/69] Genericize BroodOfCockroaches, make it one effect --- .../src/mage/cards/b/BroodOfCockroaches.java | 54 ++++--------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BroodOfCockroaches.java b/Mage.Sets/src/mage/cards/b/BroodOfCockroaches.java index 28b4e7998f2..9e3b9fd2a2c 100644 --- a/Mage.Sets/src/mage/cards/b/BroodOfCockroaches.java +++ b/Mage.Sets/src/mage/cards/b/BroodOfCockroaches.java @@ -1,26 +1,20 @@ package mage.cards.b; import mage.MageInt; -import mage.abilities.Ability; -import mage.abilities.DelayedTriggeredAbility; import mage.abilities.common.PutIntoGraveFromBattlefieldSourceTriggeredAbility; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; -import mage.abilities.effects.Effect; -import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateDelayedTriggeredAbilityEffect; import mage.abilities.effects.common.LoseLifeSourceControllerEffect; -import mage.abilities.effects.common.ReturnToHandTargetEffect; +import mage.abilities.effects.common.ReturnSourceFromGraveyardToHandEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.Outcome; import mage.constants.SubType; -import mage.game.Game; -import mage.target.targetpointer.FixedTarget; import java.util.UUID; /** - * @author mpouedras + * @author mpouedras, notgreat */ public final class BroodOfCockroaches extends CardImpl { @@ -33,7 +27,14 @@ public final class BroodOfCockroaches extends CardImpl { // When Brood of Cockroaches is put into your graveyard from the battlefield, // at the beginning of the next end step, you lose 1 life and return Brood of Cockroaches to your hand. - this.addAbility(new PutIntoGraveFromBattlefieldSourceTriggeredAbility(new BroodOfCockroachesEffect(), false, true)); + + AtTheBeginOfNextEndStepDelayedTriggeredAbility delayed = + new AtTheBeginOfNextEndStepDelayedTriggeredAbility(new LoseLifeSourceControllerEffect(1)); + + delayed.addEffect(new ReturnSourceFromGraveyardToHandEffect().concatBy("and")); + CreateDelayedTriggeredAbilityEffect delayedEffect = new CreateDelayedTriggeredAbilityEffect(delayed); + + this.addAbility(new PutIntoGraveFromBattlefieldSourceTriggeredAbility(delayedEffect, false, true)); } private BroodOfCockroaches(final BroodOfCockroaches card) { @@ -45,36 +46,3 @@ public final class BroodOfCockroaches extends CardImpl { return new BroodOfCockroaches(this); } } - -class BroodOfCockroachesEffect extends OneShotEffect { - private static final String effectText = "at the beginning of the next end step, you lose 1 life and return {this} to your hand."; - - BroodOfCockroachesEffect() { - super(Outcome.Benefit); - staticText = effectText; - } - - private BroodOfCockroachesEffect(final BroodOfCockroachesEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - AtTheBeginOfNextEndStepDelayedTriggeredAbility delayedLifeLost = - new AtTheBeginOfNextEndStepDelayedTriggeredAbility(new LoseLifeSourceControllerEffect(1)); - game.addDelayedTriggeredAbility(delayedLifeLost, source); - - Effect effect = new ReturnToHandTargetEffect(); - effect.setText("return {this} to your hand."); - effect.setTargetPointer(new FixedTarget(source.getSourceId(), source.getSourceObjectZoneChangeCounter())); - DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect); - game.addDelayedTriggeredAbility(delayedAbility, source); - - return true; - } - - @Override - public BroodOfCockroachesEffect copy() { - return new BroodOfCockroachesEffect(this); - } -} From 17e0c82a075e8752763b78f1648df4ca3acffd20 Mon Sep 17 00:00:00 2001 From: Steven Knipe Date: Thu, 20 Feb 2025 18:27:17 -0800 Subject: [PATCH 22/69] Remove BroodofCockroaches text test (in Verify), uncapitalize CreateDelayedTriggeredAbilityEffect ability rule text --- .../src/mage/cards/b/BroodOfCockroaches.java | 3 +-- .../cards/single/vis/BroodOfCockroachesTest.java | 15 --------------- .../CreateDelayedTriggeredAbilityEffect.java | 3 ++- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BroodOfCockroaches.java b/Mage.Sets/src/mage/cards/b/BroodOfCockroaches.java index 9e3b9fd2a2c..c5ab56fac92 100644 --- a/Mage.Sets/src/mage/cards/b/BroodOfCockroaches.java +++ b/Mage.Sets/src/mage/cards/b/BroodOfCockroaches.java @@ -30,8 +30,7 @@ public final class BroodOfCockroaches extends CardImpl { AtTheBeginOfNextEndStepDelayedTriggeredAbility delayed = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(new LoseLifeSourceControllerEffect(1)); - - delayed.addEffect(new ReturnSourceFromGraveyardToHandEffect().concatBy("and")); + delayed.addEffect(new ReturnSourceFromGraveyardToHandEffect().concatBy("and").setText("return this card to your hand")); CreateDelayedTriggeredAbilityEffect delayedEffect = new CreateDelayedTriggeredAbilityEffect(delayed); this.addAbility(new PutIntoGraveFromBattlefieldSourceTriggeredAbility(delayedEffect, false, true)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/vis/BroodOfCockroachesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/vis/BroodOfCockroachesTest.java index a9a30055eed..f682f4abcbd 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/vis/BroodOfCockroachesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/vis/BroodOfCockroachesTest.java @@ -1,13 +1,11 @@ package org.mage.test.cards.single.vis; -import mage.game.permanent.Permanent; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; import static mage.constants.PhaseStep.*; import static mage.constants.Zone.BATTLEFIELD; import static mage.constants.Zone.HAND; -import static org.junit.Assert.assertEquals; public class BroodOfCockroachesTest extends CardTestPlayerBase { @@ -16,19 +14,6 @@ public class BroodOfCockroachesTest extends CardTestPlayerBase { private static final String BROOD_OF_COCKROACHES = "Brood of Cockroaches"; private static final String SHOCK = "Shock"; - @Test - public void should_display_correct_text() { - String expectedText = "When {this} is put into your graveyard from the battlefield, at the beginning of the next end step, you lose 1 life and return {this} to your hand."; - - playerA_casts_Brood_of_Cockroaches_at_precombat_main_phase(); - - setStopAt(TURN_1, END_TURN); - execute(); - - Permanent permanent = getPermanent(BROOD_OF_COCKROACHES, playerA); - assertEquals(expectedText, permanent.getAbilities().get(1).toString()); - } - @Test public void should_reduce_life_of_playerA_by_1_at_the_beginning_of_the_next_end_step() { setLife(playerA, ANY_LIFE_TOTAL); diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java index a4c20b5618a..06c1ce9a490 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java @@ -67,7 +67,8 @@ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect { return staticText; } if (ability.getRuleVisible()) { - return rulePrefix + ability.getRule(); + String tmp = ability.getRule(); + return rulePrefix + tmp.substring(0, 1).toLowerCase() + tmp.substring(1); } else { return ""; } From e22a3ec19f04eb1ed6d419461e68fefc14dddfd0 Mon Sep 17 00:00:00 2001 From: Steven Knipe Date: Thu, 20 Feb 2025 21:54:34 -0800 Subject: [PATCH 23/69] Use CardUtil function instead of reimplementing lower-casing --- .../effects/common/CreateDelayedTriggeredAbilityEffect.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java index 06c1ce9a490..fd9ed392471 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CreateDelayedTriggeredAbilityEffect.java @@ -7,6 +7,7 @@ import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.game.Game; import mage.target.targetpointer.TargetPointer; +import mage.util.CardUtil; /** * @author BetaSteward_at_googlemail.com @@ -67,8 +68,7 @@ public class CreateDelayedTriggeredAbilityEffect extends OneShotEffect { return staticText; } if (ability.getRuleVisible()) { - String tmp = ability.getRule(); - return rulePrefix + tmp.substring(0, 1).toLowerCase() + tmp.substring(1); + return rulePrefix + CardUtil.getTextWithFirstCharLowerCase(ability.getRule()); } else { return ""; } From b82c6010c2231cf2fc71b7c3f29cb6cbaa0de721 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 22 Feb 2025 07:57:53 +0400 Subject: [PATCH 24/69] refactor: simplified turn logs, removed useless poison info from it --- .../src/mage/cards/b/BaruWurmspeaker.java | 2 +- Mage/src/main/java/mage/game/turn/Turn.java | 36 ++++++++----------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java b/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java index 0be6deacc9a..d87af6d364b 100644 --- a/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java +++ b/Mage.Sets/src/mage/cards/b/BaruWurmspeaker.java @@ -61,7 +61,7 @@ public final class BaruWurmspeaker extends CardImpl { ).setText("and have trample")); this.addAbility(ability); - // {7}{G}, {T}: Create a 4/4 green Wurm creature token. This ability costs {X} less to activate, whre X is the greatest power among Wurms you control. + // {7}{G}, {T}: Create a 4/4 green Wurm creature token. This ability costs {X} less to activate, where X is the greatest power among Wurms you control. ability = new SimpleActivatedAbility(new CreateTokenEffect(new Wurm44Token()), new ManaCostsImpl<>("{7}{G}")); ability.addCost(new TapSourceCost()); ability.addEffect(new InfoEffect("this ability costs {X} less to activate, " + diff --git a/Mage/src/main/java/mage/game/turn/Turn.java b/Mage/src/main/java/mage/game/turn/Turn.java index f163c2e7e9c..03966273caf 100644 --- a/Mage/src/main/java/mage/game/turn/Turn.java +++ b/Mage/src/main/java/mage/game/turn/Turn.java @@ -3,7 +3,6 @@ package mage.game.turn; import mage.abilities.Ability; import mage.constants.PhaseStep; import mage.constants.TurnPhase; -import mage.counters.CounterType; import mage.game.Game; import mage.game.events.PhaseChangedEvent; import mage.game.permanent.Permanent; @@ -17,6 +16,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.UUID; +import java.util.stream.Collectors; /** * @author BetaSteward_at_googlemail.com @@ -391,26 +391,18 @@ public class Turn implements Serializable { } private void logStartOfTurn(Game game, Player player) { - StringBuilder sb = new StringBuilder("Turn "); - sb.append(game.getState().getTurnNum()).append(' '); - if (game.getState().isExtraTurn()) { - sb.append("(extra) "); - } - sb.append(player.getLogName()); - sb.append(" ("); - int delimiter = game.getPlayers().size() - 1; - for (Player gamePlayer : game.getPlayers().values()) { - sb.append(gamePlayer.getLife()); - int poison = gamePlayer.getCountersCount(CounterType.POISON); - if (poison > 0) { - sb.append("[P:").append(poison).append(']'); - } - if (delimiter > 0) { - sb.append(" - "); - delimiter--; - } - } - sb.append(')'); - game.fireStatusEvent(sb.toString(), true, false); + // example: 0:40: TURN 1 for Human (40 - 40) + + String infoTurn = String.format("TURN %d%s for %s", + game.getState().getTurnNum(), + game.getState().isExtraTurn() ? " (extra)" : "", + player.getLogName() + ); + + String infoLife = game.getPlayers().values().stream() + .map(p -> String.valueOf(p.getLife())) + .collect(Collectors.joining(" - ")); + + game.fireStatusEvent(infoTurn + " " + infoLife, true, false); } } From 869cf83fd6f13d13cbcd2f09a309bfaaf73ca417 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 22 Feb 2025 14:56:06 +0400 Subject: [PATCH 25/69] refactor: code clean in turn --- Mage/src/main/java/mage/game/GameImpl.java | 2 +- Mage/src/main/java/mage/game/turn/Turn.java | 34 +++++++++------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index c60f6499165..17238d3c262 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -3814,7 +3814,7 @@ public abstract class GameImpl implements Game { @Override public boolean endTurn(Ability source) { - getTurn().endTurn(this, getActivePlayerId(), source); + getTurn().endTurn(this, source); return true; } diff --git a/Mage/src/main/java/mage/game/turn/Turn.java b/Mage/src/main/java/mage/game/turn/Turn.java index 03966273caf..3aa40f53653 100644 --- a/Mage/src/main/java/mage/game/turn/Turn.java +++ b/Mage/src/main/java/mage/game/turn/Turn.java @@ -53,13 +53,6 @@ public class Turn implements Serializable { } - public TurnPhase getPhaseType() { - if (currentPhase != null) { - return currentPhase.getType(); - } - return null; - } - public Phase getPhase() { return currentPhase; } @@ -85,14 +78,9 @@ public class Turn implements Serializable { } /** - * @param game - * @param activePlayer * @return true if turn is skipped */ public boolean play(Game game, Player activePlayer) { - // uncomment this to trace triggered abilities and/or continous effects - // TraceUtil.traceTriggeredAbilities(game); - // game.getState().getContinuousEffects().traceContinuousEffects(game); activePlayer.becomesActivePlayer(); this.setDeclareAttackersStepStarted(false); if (game.isPaused() || game.checkIfGameIsOver()) { @@ -150,7 +138,12 @@ public class Turn implements Serializable { game.saveState(false); //20091005 - 500.8 - while (playExtraPhases(game, phase.getType())) ; + while (true) { + // TODO: make sure it work fine (without freeze) on game errors inside extra phases + if (!playExtraPhases(game, phase.getType())) { + break; + } + } } return false; } @@ -158,7 +151,6 @@ public class Turn implements Serializable { public void resumePlay(Game game, boolean wasPaused) { activePlayerId = game.getActivePlayerId(); Player activePlayer = game.getPlayer(activePlayerId); - UUID priorityPlayerId = game.getPriorityPlayerId(); TurnPhase needPhaseType = game.getTurnPhaseType(); PhaseStep needStepType = game.getTurnStepType(); @@ -259,10 +251,16 @@ public class Turn implements Serializable { } } + /** + * Play additional phases one by one + * + * @return false to finish + */ private boolean playExtraPhases(Game game, TurnPhase afterPhase) { while (true) { TurnMod extraPhaseMod = game.getState().getTurnMods().useNextExtraPhase(activePlayerId, afterPhase); if (extraPhaseMod == null) { + // no more extra phases return false; } TurnPhase extraPhase = extraPhaseMod.getExtraPhase(); @@ -316,12 +314,8 @@ public class Turn implements Serializable { /** * Used for some spells with end turn effect (e.g. Time Stop). - * - * @param game - * @param activePlayerId - * @param source */ - public void endTurn(Game game, UUID activePlayerId, Ability source) { + public void endTurn(Game game, Ability source) { // Ending the turn this way (Time Stop) means the following things happen in order: setEndTurnRequested(true); @@ -403,6 +397,6 @@ public class Turn implements Serializable { .map(p -> String.valueOf(p.getLife())) .collect(Collectors.joining(" - ")); - game.fireStatusEvent(infoTurn + " " + infoLife, true, false); + game.fireStatusEvent(infoTurn + " (" + infoLife + ")", true, false); } } From 90034fe9b069e6a56457ed3b8f970f42b21194cf Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 22 Feb 2025 15:32:30 +0400 Subject: [PATCH 26/69] refactor: removed outdated and unused debug code (combat and continuous effects) --- .../abilities/effects/ContinuousEffects.java | 83 ------ .../main/java/mage/game/combat/Combat.java | 5 +- .../main/java/mage/util/trace/TraceInfo.java | 85 ------ .../main/java/mage/util/trace/TraceUtil.java | 241 ------------------ 4 files changed, 1 insertion(+), 413 deletions(-) delete mode 100644 Mage/src/main/java/mage/util/trace/TraceInfo.java delete mode 100644 Mage/src/main/java/mage/util/trace/TraceUtil.java diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java index 294ad8831cb..ea47fe4f39e 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffects.java @@ -22,7 +22,6 @@ import mage.game.stack.Spell; import mage.players.ManaPoolItem; import mage.players.Player; import mage.target.common.TargetCardInHand; -import mage.util.trace.TraceInfo; import org.apache.log4j.Logger; import java.io.Serializable; @@ -1405,88 +1404,6 @@ public class ContinuousEffects implements Serializable { return controllerFound; } - /** - * Debug only: prints out a status of the currently existing continuous effects - * - * @param game - */ - public void traceContinuousEffects(Game game) { - game.getContinuousEffects().getLayeredEffects(game); - logger.info("-------------------------------------------------------------------------------------------------"); - int numberEffects = 0; - for (ContinuousEffectsList list : allEffectsLists) { - numberEffects += list.size(); - } - logger.info("Turn: " + game.getTurnNum() + " - currently existing continuous effects: " + numberEffects); - logger.info("layeredEffects ...................: " + layeredEffects.size()); - logger.info("continuousRuleModifyingEffects ...: " + continuousRuleModifyingEffects.size()); - logger.info("replacementEffects ...............: " + replacementEffects.size()); - logger.info("preventionEffects ................: " + preventionEffects.size()); - logger.info("requirementEffects ...............: " + requirementEffects.size()); - logger.info("restrictionEffects ...............: " + restrictionEffects.size()); - logger.info("restrictionUntapNotMoreThanEffects: " + restrictionUntapNotMoreThanEffects.size()); - logger.info("costModificationEffects ..........: " + costModificationEffects.size()); - logger.info("spliceCardEffects ................: " + spliceCardEffects.size()); - logger.info("asThoughEffects:"); - for (Map.Entry> entry : asThoughEffectsMap.entrySet()) { - logger.info("... " + entry.getKey().toString() + ": " + entry.getValue().size()); - } - logger.info("applyStatus ....................: " + (applyStatus != null ? "exists" : "null")); - logger.info("auraReplacementEffect ............: " + (continuousRuleModifyingEffects != null ? "exists" : "null")); - Map orderedEffects = new TreeMap<>(); - traceAddContinuousEffects(orderedEffects, layeredEffects, game, "layeredEffects................"); - traceAddContinuousEffects(orderedEffects, continuousRuleModifyingEffects, game, "continuousRuleModifyingEffects"); - traceAddContinuousEffects(orderedEffects, replacementEffects, game, "replacementEffects............"); - traceAddContinuousEffects(orderedEffects, preventionEffects, game, "preventionEffects............."); - traceAddContinuousEffects(orderedEffects, requirementEffects, game, "requirementEffects............"); - traceAddContinuousEffects(orderedEffects, restrictionEffects, game, "restrictionEffects............"); - traceAddContinuousEffects(orderedEffects, restrictionUntapNotMoreThanEffects, game, "restrictionUntapNotMore..."); - traceAddContinuousEffects(orderedEffects, costModificationEffects, game, "costModificationEffects......."); - traceAddContinuousEffects(orderedEffects, spliceCardEffects, game, "spliceCardEffects............."); - for (Map.Entry> entry : asThoughEffectsMap.entrySet()) { - traceAddContinuousEffects(orderedEffects, entry.getValue(), game, entry.getKey().toString()); - } - String playerName = ""; - for (Map.Entry entry : orderedEffects.entrySet()) { - if (!entry.getValue().getPlayerName().equals(playerName)) { - playerName = entry.getValue().getPlayerName(); - logger.info("--- Player: " + playerName + " --------------------------------"); - } - logger.info(entry.getValue().getInfo() - + " " + entry.getValue().getSourceName() - + " " + entry.getValue().getDuration().name() - + " " + entry.getValue().getRule() - + " (Order: " + entry.getValue().getOrder() + ")" - ); - } - logger.info("---- End trace Continuous effects --------------------------------------------------------------------------"); - } - - public static void traceAddContinuousEffects(Map orderedEffects, ContinuousEffectsList cel, Game game, String listName) { - for (ContinuousEffect effect : cel) { - Set abilities = cel.getAbility(effect.getId()); - for (Ability ability : abilities) { - Player controller = game.getPlayer(ability.getControllerId()); - MageObject source = game.getObject(ability.getSourceId()); - TraceInfo traceInfo = new TraceInfo(); - traceInfo.setInfo(listName); - traceInfo.setOrder(effect.getOrder()); - if (ability instanceof MageSingleton) { - traceInfo.setPlayerName("Mage Singleton"); - traceInfo.setSourceName("Mage Singleton"); - } else { - traceInfo.setPlayerName(controller == null ? "no controller" : controller.getName()); - traceInfo.setSourceName(source == null ? "no source" : source.getIdName()); - } - traceInfo.setRule(ability.getRule()); - traceInfo.setAbilityId(ability.getId()); - traceInfo.setEffectId(effect.getId()); - traceInfo.setDuration(effect.getDuration()); - orderedEffects.put(traceInfo.getPlayerName() + traceInfo.getSourceName() + effect.getId() + ability.getId(), traceInfo); - } - } - } - public int getTotalEffectsCount() { return allEffectsLists.stream().mapToInt(ContinuousEffectsList::size).sum(); } diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 5a433b7e04f..91637785533 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -33,7 +33,6 @@ import mage.target.common.TargetControlledPermanent; import mage.target.common.TargetDefender; import mage.util.CardUtil; import mage.util.Copyable; -import mage.util.trace.TraceUtil; import org.apache.log4j.Logger; import java.io.Serializable; @@ -744,8 +743,6 @@ public class Combat implements Serializable, Copyable { game.getCombat().logBlockerInfo(defender, game); } } - // tool to catch the bug about flyers blocked by non flyers or intimidate blocked by creatures with other colors - TraceUtil.traceCombatIfNeeded(game, game.getCombat()); } private void makeSureItsNotComputer(Player controller) { @@ -761,7 +758,7 @@ public class Combat implements Serializable, Copyable { * Add info about attacker blocked by blocker to the game log */ private void logBlockerInfo(Player defender, Game game) { - boolean shownDefendingPlayer = game.getPlayers().size() < 3; // only two players no need to saw the attacked player + boolean shownDefendingPlayer = game.getPlayers().size() <= 2; // 1 vs 1 game, no need to saw the attacked player for (CombatGroup group : game.getCombat().getGroups()) { if (group.defendingPlayerId.equals(defender.getId())) { if (!shownDefendingPlayer) { diff --git a/Mage/src/main/java/mage/util/trace/TraceInfo.java b/Mage/src/main/java/mage/util/trace/TraceInfo.java deleted file mode 100644 index fd282f6f76c..00000000000 --- a/Mage/src/main/java/mage/util/trace/TraceInfo.java +++ /dev/null @@ -1,85 +0,0 @@ -package mage.util.trace; - -import java.util.UUID; -import mage.constants.Duration; - -/** - * - * @author LevelX2 - */ -public class TraceInfo { - public String info; - public String playerName; - public String sourceName; - public String rule; - public UUID abilityId; - public UUID effectId; - public Duration duration; - public long order; - - public String getPlayerName() { - return playerName; - } - - public void setPlayerName(String playerName) { - this.playerName = playerName; - } - - public String getSourceName() { - return sourceName; - } - - public void setSourceName(String sourceName) { - this.sourceName = sourceName; - } - - public String getRule() { - return rule; - } - - public void setRule(String rule) { - this.rule = rule; - } - - public UUID getAbilityId() { - return abilityId; - } - - public void setAbilityId(UUID abilityId) { - this.abilityId = abilityId; - } - - public UUID getEffectId() { - return effectId; - } - - public void setEffectId(UUID effectId) { - this.effectId = effectId; - } - - public String getInfo() { - return info; - } - - public void setInfo(String info) { - this.info = info; - } - - public Duration getDuration() { - return duration; - } - - public void setDuration(Duration duration) { - this.duration = duration; - } - - public long getOrder() { - return order; - } - - public void setOrder(long order) { - this.order = order; - } - - -} diff --git a/Mage/src/main/java/mage/util/trace/TraceUtil.java b/Mage/src/main/java/mage/util/trace/TraceUtil.java deleted file mode 100644 index c5865f635fb..00000000000 --- a/Mage/src/main/java/mage/util/trace/TraceUtil.java +++ /dev/null @@ -1,241 +0,0 @@ -package mage.util.trace; - -import java.util.*; -import mage.MageObject; -import mage.abilities.Ability; -import mage.abilities.StaticAbility; -import mage.abilities.TriggeredAbility; -import mage.abilities.effects.ContinuousEffectsList; -import mage.abilities.effects.RestrictionEffect; -import mage.abilities.keyword.CantBeBlockedSourceAbility; -import mage.abilities.keyword.FlyingAbility; -import mage.abilities.keyword.IntimidateAbility; -import mage.abilities.keyword.ReachAbility; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.combat.Combat; -import mage.game.combat.CombatGroup; -import mage.game.permanent.Permanent; -import mage.players.Player; -import org.apache.log4j.Logger; - -/** - * @author magenoxx_at_gmail.com - */ -public final class TraceUtil { - - private static final Logger log = Logger.getLogger(TraceUtil.class); - - /** - * This method is intended to catch various bugs with combat. - * - * One of them (possibly the most annoying) is when creature without flying or reach blocks creature with flying. - * No test managed to reproduce it, but it happens in the games time to time and was reported by different players. - * - * The idea: is to catch such cases manually and print out as much information from game state that may help as possible. - * @param game - * @param combat - */ - public static void traceCombatIfNeeded(Game game, Combat combat) { - // trace non-flying vs flying - for (CombatGroup group : combat.getGroups()) { - for (UUID attackerId : group.getAttackers()) { - Permanent attacker = game.getPermanent(attackerId); - if (attacker != null) { - if (hasFlying(attacker)) { - // traceCombat(game, attacker, null); - for (UUID blockerId : group.getBlockers()) { - Permanent blocker = game.getPermanent(blockerId); - if (blocker != null && !hasFlying(blocker) && !hasReach(blocker)) { - log.warn("Found non-flying non-reach creature blocking creature with flying"); - traceCombat(game, attacker, blocker); - } - } - } - if (hasIntimidate(attacker)) { - for (UUID blockerId : group.getBlockers()) { - Permanent blocker = game.getPermanent(blockerId); - if (blocker != null && !blocker.isArtifact(game) - && !attacker.getColor(game).shares(blocker.getColor(game))) { - log.warn("Found creature with intimidate blocked by non artifact not sharing color creature"); - traceCombat(game, attacker, blocker); - } - } - } - if (cantBeBlocked(attacker)) { - if (!group.getBlockers().isEmpty()) { - Permanent blocker = game.getPermanent(group.getBlockers().get(0)); - if (blocker != null) { - log.warn("Found creature that can't be blocked by some other creature"); - traceCombat(game, attacker, blocker); - } - } - } - } - } - } - } - - /** - * We need this to check Flying existence in not-common way: by instanceof. - * @return - */ - private static boolean hasFlying(Permanent permanent) { - for (Ability ability : permanent.getAbilities()) { - if (ability instanceof FlyingAbility) { - return true; - } - } - return false; - } - - private static boolean hasIntimidate(Permanent permanent) { - for (Ability ability : permanent.getAbilities()) { - if (ability instanceof IntimidateAbility) { - return true; - } - } - return false; - } - - private static boolean hasReach(Permanent permanent) { - for (Ability ability : permanent.getAbilities()) { - if (ability instanceof ReachAbility) { - return true; - } - } - return false; - } - - private static boolean cantBeBlocked(Permanent permanent) { - for (Ability ability : permanent.getAbilities()) { - if (ability instanceof CantBeBlockedSourceAbility) { - return true; - } - } - return false; - } - - private static void traceCombat(Game game, Permanent attacker, Permanent blocker) { - String prefix = "> "; - log.error(prefix+"Tracing game state..."); - if (blocker != null) { - log.error(prefix+blocker.getLogName() + " could block " + attacker.getLogName()); - } - - log.error(prefix); - log.error(prefix+"Attacker abilities: "); - for (Ability ability : attacker.getAbilities()) { - log.error(prefix+" " + ability.toString() + ", id=" + ability.getId()); - } - if (blocker != null) { - log.error(prefix+"Blocker abilities: "); - for (Ability ability : blocker.getAbilities()) { - log.error(prefix+" " + ability.toString() + ", id=" + ability.getId()); - } - } - - log.error(prefix); - log.error(prefix+"Flying ability id: " + FlyingAbility.getInstance().getId()); - log.error(prefix+"Reach ability id: " + ReachAbility.getInstance().getId()); - log.error(prefix+"Intimidate ability id: " + IntimidateAbility.getInstance().getId()); - log.error(prefix); - - log.error(prefix+"Restriction effects:"); - log.error(prefix+" Applied to ATTACKER:"); - Map> attackerResEffects = game.getContinuousEffects().getApplicableRestrictionEffects(attacker, game); - for (Map.Entry> entry : attackerResEffects.entrySet()) { - log.error(prefix+" " + entry.getKey()); - log.error(prefix+" id=" + entry.getKey().getId()); - for (Ability ability: entry.getValue()) { - log.error(prefix+" ability=" + ability); - } - } - log.error(prefix+" Applied to BLOCKER:"); - if (blocker != null) { - Map> blockerResEffects = game.getContinuousEffects().getApplicableRestrictionEffects(blocker, game); - for (Map.Entry> entry : blockerResEffects.entrySet()) { - log.error(prefix+" " + entry.getKey()); - log.error(prefix+" id=" + entry.getKey().getId()); - for (Ability ability: entry.getValue()) { - log.error(prefix+" ability=" + ability); - } - } - } - ContinuousEffectsList restrictionEffects = (ContinuousEffectsList) game.getContinuousEffects().getRestrictionEffects(); - log.error(prefix); - log.error(prefix+" List of all restriction effects:"); - for (RestrictionEffect effect : restrictionEffects) { - log.error(prefix+" " + effect); - log.error(prefix+" id=" + effect.getId()); - } - - log.error(prefix); - log.error(prefix+" Trace Attacker:"); - traceForPermanent(game, attacker, prefix, restrictionEffects); - if (blocker != null) { - log.error(prefix); - log.error(prefix+" Trace Blocker:"); - traceForPermanent(game, blocker, prefix, restrictionEffects); - } - - log.error(prefix); - } - - private static void traceForPermanent(Game game, Permanent permanent, String uuid, ContinuousEffectsList restrictionEffects) { - for (RestrictionEffect effect: restrictionEffects) { - log.error(uuid+" effect=" + effect.toString() + " id=" + effect.getId()); - for (Ability ability : restrictionEffects.getAbility(effect.getId())) { - if (!(ability instanceof StaticAbility) || ability.isInUseableZone(game, permanent, null)) { - log.error(uuid+" ability=" + ability + ", applies_to_attacker=" + effect.applies(permanent, ability, game)); - } else { - boolean usable = ability.isInUseableZone(game, permanent, null); - log.error(uuid+" instanceof StaticAbility: " + (ability instanceof StaticAbility) + ", ability=" + ability); - log.error(uuid+" usable zone: " + usable + ", ability=" + ability); - if (!usable) { - Zone zone = ability.getZone(); - log.error(uuid+" zone: " + zone); - MageObject object = game.getObject(ability.getSourceId()); - log.error(uuid+" object: " + object); - if (object != null) { - log.error(uuid + " contains ability: " + object.getAbilities().contains(ability)); - } - Zone test = game.getState().getZone(ability.getSourceId()); - log.error(uuid+" test_zone: " + test); - } - } - } - } - } - - public static void trace(String msg) { - log.info(msg); - } - - /** - * Prints out a status of the currently existing triggered abilities - * @param game - */ - public static void traceTriggeredAbilities(Game game) { - log.info("-------------------------------------------------------------------------------------------------"); - log.info("Turn: " + game.getTurnNum() + " - currently existing triggered abilities: " + game.getState().getTriggers().size()); - Map orderedAbilities = new TreeMap<>(); - for (Map.Entry entry : game.getState().getTriggers().entrySet()) { - Player controller = game.getPlayer(entry.getValue().getControllerId()); - MageObject source = game.getObject(entry.getValue().getSourceId()); - orderedAbilities.put((controller == null ? "no controller": controller.getName()) + (source == null ? "no source": source.getIdName())+ entry.getKey(), entry.getKey()); - } - String playerName = ""; - for (Map.Entry entry : orderedAbilities.entrySet()) { - TriggeredAbility trAbility = game.getState().getTriggers().get(entry.getValue()); - Player controller = game.getPlayer(trAbility.getControllerId()); - MageObject source = game.getObject(trAbility.getSourceId()); - if (!controller.getName().equals(playerName)) { - playerName = controller.getName(); - log.info("--- Player: " + playerName + " --------------------------------"); - } - log.info((source == null ? "no source": source.getIdName()) + " -> " - + trAbility.getRule()); - } - } -} From a7d5526c390e8274866871386c75496bc38ef9d7 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 22 Feb 2025 20:16:07 +0400 Subject: [PATCH 27/69] Gnostro, Voice of the Crags - added card hint --- Mage.Sets/src/mage/cards/g/GnostroVoiceOfTheCrags.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/g/GnostroVoiceOfTheCrags.java b/Mage.Sets/src/mage/cards/g/GnostroVoiceOfTheCrags.java index 30e21239d8c..4452a3e2678 100644 --- a/Mage.Sets/src/mage/cards/g/GnostroVoiceOfTheCrags.java +++ b/Mage.Sets/src/mage/cards/g/GnostroVoiceOfTheCrags.java @@ -10,6 +10,7 @@ import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DamageTargetEffect; import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -39,6 +40,7 @@ public final class GnostroVoiceOfTheCrags extends CardImpl { // {T}: Choose one. X is the number of spells you've cast this turn. // • Scry X. Ability ability = new SimpleActivatedAbility(new GnostroVoiceOfTheCragsEffect(), new TapSourceCost()); + ability.addHint(new ValueHint("Number of spells you've cast this turn", GnostroVoiceOfTheCragsValue.instance)); ability.getModes().setChooseText("choose one. X is the number of spells you've cast this turn."); // • Gnostro, Voice of the Crags deals X damage to target creature. @@ -104,7 +106,7 @@ class GnostroVoiceOfTheCragsEffect extends OneShotEffect { if (player == null) { return false; } - return player.scry ( + return player.scry( GnostroVoiceOfTheCragsValue.instance.calculate(game, source, this), source, game ); } From 1a93fe97c47ca36f93803eccfa598cf21072ac03 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 22 Feb 2025 20:16:31 +0400 Subject: [PATCH 28/69] Volcanic Torrent - added card hint --- Mage.Sets/src/mage/cards/v/VolcanicTorrent.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage.Sets/src/mage/cards/v/VolcanicTorrent.java b/Mage.Sets/src/mage/cards/v/VolcanicTorrent.java index 752d57cb648..23e032c9579 100644 --- a/Mage.Sets/src/mage/cards/v/VolcanicTorrent.java +++ b/Mage.Sets/src/mage/cards/v/VolcanicTorrent.java @@ -4,6 +4,7 @@ import mage.abilities.Ability; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.abilities.effects.common.DamageAllEffect; +import mage.abilities.hint.ValueHint; import mage.abilities.keyword.CascadeAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -36,6 +37,7 @@ public final class VolcanicTorrent extends CardImpl { // Volcanic Torrent deals X damage to each creature and planeswalker your opponents control, where X is the number of spells you've cast this turn. this.getSpellAbility().addEffect(new DamageAllEffect(VolcanicTorrentValue.instance, filter)); + this.getSpellAbility().addHint(new ValueHint("Number of spells you've cast this turn", VolcanicTorrentValue.instance)); } private VolcanicTorrent(final VolcanicTorrent card) { From 8c259dace5673c2f86d4543257ae587bca6df4e8 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 22 Feb 2025 20:16:49 +0400 Subject: [PATCH 29/69] Noise Marine - added card hint --- Mage.Sets/src/mage/cards/n/NoiseMarine.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage.Sets/src/mage/cards/n/NoiseMarine.java b/Mage.Sets/src/mage/cards/n/NoiseMarine.java index 497c2414f41..139193f7e2d 100644 --- a/Mage.Sets/src/mage/cards/n/NoiseMarine.java +++ b/Mage.Sets/src/mage/cards/n/NoiseMarine.java @@ -6,6 +6,7 @@ import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.effects.Effect; import mage.abilities.effects.common.DamageTargetEffect; +import mage.abilities.hint.ValueHint; import mage.abilities.keyword.CascadeAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -38,6 +39,7 @@ public final class NoiseMarine extends CardImpl { new DamageTargetEffect(NoiseMarineValue.instance, "it") ); ability.addTarget(new TargetAnyTarget()); + ability.addHint(new ValueHint("Number of spells you've cast this turn", NoiseMarineValue.instance)); this.addAbility(ability.withFlavorWord("Sonic Blaster")); } From d5fa2736de9e4d47017bf1c2623da0df867c245a Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 19 Feb 2025 15:28:10 -0500 Subject: [PATCH 30/69] [FIN] Implement Jumbo Cactuar --- Mage.Sets/src/mage/cards/j/JumboCactuar.java | 40 ++++++++++++++++++++ Mage.Sets/src/mage/sets/FinalFantasy.java | 1 + 2 files changed, 41 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/j/JumboCactuar.java diff --git a/Mage.Sets/src/mage/cards/j/JumboCactuar.java b/Mage.Sets/src/mage/cards/j/JumboCactuar.java new file mode 100644 index 00000000000..96c178dcd04 --- /dev/null +++ b/Mage.Sets/src/mage/cards/j/JumboCactuar.java @@ -0,0 +1,40 @@ +package mage.cards.j; + +import mage.MageInt; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class JumboCactuar extends CardImpl { + + public JumboCactuar(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{G}{G}"); + + this.subtype.add(SubType.PLANT); + this.power = new MageInt(1); + this.toughness = new MageInt(7); + + // 10,000 Needles -- Whenever this creature attacks, it gets +9999/+0 until end of turn. + this.addAbility(new AttacksTriggeredAbility( + new BoostSourceEffect(9999, 0, Duration.EndOfTurn, "it") + ).withFlavorWord("10,000 Needles")); + } + + private JumboCactuar(final JumboCactuar card) { + super(card); + } + + @Override + public JumboCactuar copy() { + return new JumboCactuar(this); + } +} diff --git a/Mage.Sets/src/mage/sets/FinalFantasy.java b/Mage.Sets/src/mage/sets/FinalFantasy.java index e6b322b0176..b7670eac000 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasy.java +++ b/Mage.Sets/src/mage/sets/FinalFantasy.java @@ -20,6 +20,7 @@ public final class FinalFantasy extends ExpansionSet { this.blockName = "Final Fantasy"; // for sorting in GUI this.hasBasicLands = false; // temporary + cards.add(new SetCardInfo("Jumbo Cactuar", 191, Rarity.RARE, mage.cards.j.JumboCactuar.class)); cards.add(new SetCardInfo("Sazh's Chocobo", 200, Rarity.UNCOMMON, mage.cards.s.SazhsChocobo.class)); } } From 709e892ed78270e0feb869df167c95f5010ba01c Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 12:25:11 -0500 Subject: [PATCH 31/69] [TDM] add set --- .../dl/sources/ScryfallImageSupportCards.java | 1 + .../src/mage/sets/TarkirDragonstorm.java | 21 +++++++++++++++++++ Utils/known-sets.txt | 1 + Utils/mtg-cards-data.txt | 9 ++++++++ Utils/mtg-sets-data.txt | 1 + 5 files changed, 33 insertions(+) create mode 100644 Mage.Sets/src/mage/sets/TarkirDragonstorm.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java index 6b4a44037da..1c1873167be 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java @@ -579,6 +579,7 @@ public class ScryfallImageSupportCards { add("INR"); // Innistrad Remastered add("DFT"); // Aetherdrift add("DRC"); // Aetherdrift Commander + add("TDM"); // Tarkir: Dragonstorm add("FIN"); // Final Fantasy add("FIC"); // Final Fantasy Commander diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java new file mode 100644 index 00000000000..3b3cec0dbb1 --- /dev/null +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -0,0 +1,21 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.SetType; + +/** + * @author TheElk801 + */ +public final class TarkirDragonstorm extends ExpansionSet { + + private static final TarkirDragonstorm instance = new TarkirDragonstorm(); + + public static TarkirDragonstorm getInstance() { + return instance; + } + + private TarkirDragonstorm() { + super("Tarkir: Dragonstorm", "TDM", ExpansionSet.buildDate(2025, 4, 11), SetType.EXPANSION); + this.blockName = "Tarkir: Dragonstorm"; // for sorting in GUI + } +} diff --git a/Utils/known-sets.txt b/Utils/known-sets.txt index daa56f04537..962f3f02759 100644 --- a/Utils/known-sets.txt +++ b/Utils/known-sets.txt @@ -250,6 +250,7 @@ New Capenna Commander|NewCapennaCommander| Strixhaven: School of Mages|StrixhavenSchoolOfMages| Stronghold|Stronghold| Super Series|SuperSeries| +Tarkir: Dragonstorm|TarkirDragonstorm| Tempest|Tempest| Tempest Remastered|TempestRemastered| Tenth Edition|TenthEdition| diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 56dfafab680..3a9ddb982ba 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -57197,3 +57197,12 @@ 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.| +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| +Barrensteppe Siege|Tarkir: Dragonstorm|171|R|{2}{W}{B}|Enchantment|||As this enchantment enters, choose Abzan or Mardu.$* Abzan -- At the beginning of your end step, put a +1/+1 counter on each creature you control.$* Mardu -- At the beginning of your end step, if a creature died under your control this turn, each opponent sacrifices a creature of their choice.| +Inevitable Defeat|Tarkir: Dragonstorm|194|R|{1}{R}{W}{B}|Instant|||This spell can't be countered.$Exile target nonland permanent. Its controller loses 3 life and you gain 3 life.| +Narset, Jeskai Waymaster|Tarkir: Dragonstorm|209|R|{U}{R}{W}|Legendary Creature - Human Monk|3|4|At the beginning of your end step, you may discard your hand. If you do, draw cards equal to the number of spells you've cast this turn.| +Shiko, Paragon of the Way|Tarkir: Dragonstorm|223|M|{2}{U}{R}{W}|Legendary Creature - Spirit Dragon|4|5|Flying, vigilance$When Shiko enters, exile target nonland card with mana value 3 or less from your graveyard. Copy it, then you may cast the copy without paying its mana cost.| +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.| diff --git a/Utils/mtg-sets-data.txt b/Utils/mtg-sets-data.txt index 95822e07c65..38dda3b135d 100644 --- a/Utils/mtg-sets-data.txt +++ b/Utils/mtg-sets-data.txt @@ -248,6 +248,7 @@ New Capenna Commander|NCC| Stronghold|STH| Special Guests|SPG| Super Series|SUS| +Tarkir: Dragonstorm|TDM| Theros|THS| Theros Beyond Death|THB| The Brothers' War|BRO| From c22fadac98cc2369af889b8fa2fc26ab1dbfd2b1 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 12:27:23 -0500 Subject: [PATCH 32/69] [TDC] add set --- .../dl/sources/ScryfallImageSupportCards.java | 1 + .../mage/sets/TarkirDragonstormCommander.java | 21 +++++++++++++++++++ Utils/known-sets.txt | 1 + Utils/mtg-cards-data.txt | 1 + Utils/mtg-sets-data.txt | 1 + 5 files changed, 25 insertions(+) create mode 100644 Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java diff --git a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java index 1c1873167be..1c64712ad5a 100644 --- a/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java +++ b/Mage.Client/src/main/java/org/mage/plugins/card/dl/sources/ScryfallImageSupportCards.java @@ -580,6 +580,7 @@ public class ScryfallImageSupportCards { add("DFT"); // Aetherdrift add("DRC"); // Aetherdrift Commander add("TDM"); // Tarkir: Dragonstorm + add("TDC"); // Tarkir: Dragonstorm Commander add("FIN"); // Final Fantasy add("FIC"); // Final Fantasy Commander diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java new file mode 100644 index 00000000000..b6ce0a14269 --- /dev/null +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -0,0 +1,21 @@ +package mage.sets; + +import mage.cards.ExpansionSet; +import mage.constants.SetType; + +/** + * @author TheElk801 + */ +public final class TarkirDragonstormCommander extends ExpansionSet { + + private static final TarkirDragonstormCommander instance = new TarkirDragonstormCommander(); + + public static TarkirDragonstormCommander getInstance() { + return instance; + } + + private TarkirDragonstormCommander() { + super("Tarkir: Dragonstorm Commander", "TDC", ExpansionSet.buildDate(2025, 4, 11), SetType.SUPPLEMENTAL); + this.hasBasicLands = false; + } +} diff --git a/Utils/known-sets.txt b/Utils/known-sets.txt index 962f3f02759..ffae098c575 100644 --- a/Utils/known-sets.txt +++ b/Utils/known-sets.txt @@ -251,6 +251,7 @@ Strixhaven: School of Mages|StrixhavenSchoolOfMages| Stronghold|Stronghold| Super Series|SuperSeries| Tarkir: Dragonstorm|TarkirDragonstorm| +Tarkir: Dragonstorm Commander|TarkirDragonstormCommander| Tempest|Tempest| Tempest Remastered|TempestRemastered| Tenth Edition|TenthEdition| diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 3a9ddb982ba..edc9a529505 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -57206,3 +57206,4 @@ Narset, Jeskai Waymaster|Tarkir: Dragonstorm|209|R|{U}{R}{W}|Legendary Creature Shiko, Paragon of the Way|Tarkir: Dragonstorm|223|M|{2}{U}{R}{W}|Legendary Creature - Spirit Dragon|4|5|Flying, vigilance$When Shiko enters, exile target nonland card with mana value 3 or less from your graveyard. Copy it, then you may cast the copy without paying its mana cost.| 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.| diff --git a/Utils/mtg-sets-data.txt b/Utils/mtg-sets-data.txt index 38dda3b135d..0aee6b40f99 100644 --- a/Utils/mtg-sets-data.txt +++ b/Utils/mtg-sets-data.txt @@ -249,6 +249,7 @@ Stronghold|STH| Special Guests|SPG| Super Series|SUS| Tarkir: Dragonstorm|TDM| +Tarkir: Dragonstorm Commander|TDC| Theros|THS| Theros Beyond Death|THB| The Brothers' War|BRO| From fdc379557153325097fc881e3b3bd4a705ec90df Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 12:30:19 -0500 Subject: [PATCH 33/69] [TDM] Implement Stormscale Dragon --- .../src/mage/cards/s/StormscaleScion.java | 51 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 3 ++ 2 files changed, 54 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/StormscaleScion.java diff --git a/Mage.Sets/src/mage/cards/s/StormscaleScion.java b/Mage.Sets/src/mage/cards/s/StormscaleScion.java new file mode 100644 index 00000000000..472d34397a1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/StormscaleScion.java @@ -0,0 +1,51 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.StormAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.filter.common.FilterCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class StormscaleScion extends CardImpl { + + private static final FilterCreaturePermanent filter = new FilterCreaturePermanent(SubType.DRAGON, "Dragons"); + + public StormscaleScion(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{4}{R}{R}"); + + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Other Dragons you control get +1/+1. + this.addAbility(new SimpleStaticAbility(new BoostControlledEffect( + 1, 1, Duration.WhileOnBattlefield, filter, true + ))); + + // Storm + this.addAbility(new StormAbility()); + } + + private StormscaleScion(final StormscaleScion card) { + super(card); + } + + @Override + public StormscaleScion copy() { + return new StormscaleScion(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 3b3cec0dbb1..2dfab870960 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -1,6 +1,7 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.constants.Rarity; import mage.constants.SetType; /** @@ -17,5 +18,7 @@ public final class TarkirDragonstorm extends ExpansionSet { private TarkirDragonstorm() { super("Tarkir: Dragonstorm", "TDM", ExpansionSet.buildDate(2025, 4, 11), SetType.EXPANSION); this.blockName = "Tarkir: Dragonstorm"; // for sorting in GUI + + cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); } } From e69b80df914cbd748c6d9dc2888f573faab77662 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 12:34:09 -0500 Subject: [PATCH 34/69] [TDM] Implement Mox Jasper --- Mage.Sets/src/mage/cards/m/MoxJasper.java | 50 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 51 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MoxJasper.java diff --git a/Mage.Sets/src/mage/cards/m/MoxJasper.java b/Mage.Sets/src/mage/cards/m/MoxJasper.java new file mode 100644 index 00000000000..a5d1cffc6f1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MoxJasper.java @@ -0,0 +1,50 @@ +package mage.cards.m; + +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.mana.AddManaOfAnyColorEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.abilities.mana.ActivateIfConditionManaAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.filter.common.FilterControlledPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MoxJasper extends CardImpl { + + private static final Condition condition = new PermanentsOnTheBattlefieldCondition( + new FilterControlledPermanent(SubType.DRAGON, "you control a Dragon") + ); + private static final Hint hint = new ConditionHint(condition, "You control a Dragon"); + + public MoxJasper(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{0}"); + + this.supertype.add(SuperType.LEGENDARY); + + // {T}: Add one mana of any color. Activate only if you control a Dragon. + this.addAbility(new ActivateIfConditionManaAbility( + Zone.BATTLEFIELD, new AddManaOfAnyColorEffect(1), + new TapSourceCost(), condition + ).addHint(hint)); + } + + private MoxJasper(final MoxJasper card) { + super(card); + } + + @Override + public MoxJasper copy() { + return new MoxJasper(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 2dfab870960..bf2aaf21145 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -19,6 +19,7 @@ public final class TarkirDragonstorm extends ExpansionSet { super("Tarkir: Dragonstorm", "TDM", ExpansionSet.buildDate(2025, 4, 11), SetType.EXPANSION); this.blockName = "Tarkir: Dragonstorm"; // for sorting in GUI + cards.add(new SetCardInfo("Mox Jasper", 246, Rarity.MYTHIC, mage.cards.m.MoxJasper.class)); cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); } } From 4276b4f0bca6a0f2360f82f060c45f382ee729dd Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 12:35:03 -0500 Subject: [PATCH 35/69] [TDM] Implement Skirmish Rhino --- Mage.Sets/src/mage/cards/s/SkirmishRhino.java | 45 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 46 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SkirmishRhino.java diff --git a/Mage.Sets/src/mage/cards/s/SkirmishRhino.java b/Mage.Sets/src/mage/cards/s/SkirmishRhino.java new file mode 100644 index 00000000000..719ddaa4999 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SkirmishRhino.java @@ -0,0 +1,45 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.abilities.keyword.TrampleAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SkirmishRhino extends CardImpl { + + public SkirmishRhino(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}{B}{G}"); + + this.subtype.add(SubType.RHINO); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // Trample + this.addAbility(TrampleAbility.getInstance()); + + // When this creature enters, each opponent loses 2 life and you gain 2 life. + Ability ability = new EntersBattlefieldTriggeredAbility(new LoseLifeOpponentsEffect(2)); + ability.addEffect(new GainLifeEffect(2).concatBy("and")); + this.addAbility(ability); + } + + private SkirmishRhino(final SkirmishRhino card) { + super(card); + } + + @Override + public SkirmishRhino copy() { + return new SkirmishRhino(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index bf2aaf21145..2b48b2570c0 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -20,6 +20,7 @@ public final class TarkirDragonstorm extends ExpansionSet { this.blockName = "Tarkir: Dragonstorm"; // for sorting in GUI cards.add(new SetCardInfo("Mox Jasper", 246, Rarity.MYTHIC, mage.cards.m.MoxJasper.class)); + cards.add(new SetCardInfo("Skirmish Rhino", 224, Rarity.UNCOMMON, mage.cards.s.SkirmishRhino.class)); cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); } } From 9c29ff9ca4559e813dc2ce1d4ac118108c13a991 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 12:42:24 -0500 Subject: [PATCH 36/69] [TDC] Implement Teval, the Balanced Scale --- .../mage/cards/t/TevalTheBalancedScale.java | 92 +++++++++++++++++++ .../mage/sets/TarkirDragonstormCommander.java | 3 + .../permanent/token/ZombieDruidToken.java | 30 ++++++ 3 files changed, 125 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/TevalTheBalancedScale.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/ZombieDruidToken.java diff --git a/Mage.Sets/src/mage/cards/t/TevalTheBalancedScale.java b/Mage.Sets/src/mage/cards/t/TevalTheBalancedScale.java new file mode 100644 index 00000000000..d58272b43f6 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TevalTheBalancedScale.java @@ -0,0 +1,92 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.CardsLeaveGraveyardTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.MillCardsControllerEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.token.ZombieDruidToken; +import mage.players.Player; +import mage.target.TargetCard; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TevalTheBalancedScale extends CardImpl { + + public TevalTheBalancedScale(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B}{G}{U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever Teval attacks, mill three cards. Then you may return a land card from your graveyard to the battlefield tapped. + Ability ability = new AttacksTriggeredAbility(new MillCardsControllerEffect(3)); + ability.addEffect(new TevalTheBalancedScaleEffect()); + this.addAbility(ability); + + // Whenever one or more cards leave your graveyard, create a 2/2 black Zombie Druid creature token. + this.addAbility(new CardsLeaveGraveyardTriggeredAbility(new CreateTokenEffect(new ZombieDruidToken()))); + } + + private TevalTheBalancedScale(final TevalTheBalancedScale card) { + super(card); + } + + @Override + public TevalTheBalancedScale copy() { + return new TevalTheBalancedScale(this); + } +} + +class TevalTheBalancedScaleEffect extends OneShotEffect { + + TevalTheBalancedScaleEffect() { + super(Outcome.Benefit); + staticText = "Then you may return a land card from your graveyard to the battlefield tapped"; + } + + private TevalTheBalancedScaleEffect(final TevalTheBalancedScaleEffect effect) { + super(effect); + } + + @Override + public TevalTheBalancedScaleEffect copy() { + return new TevalTheBalancedScaleEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + TargetCard target = new TargetCardInYourGraveyard( + 0, 1, StaticFilters.FILTER_CARD_LAND, true + ); + player.choose(outcome, player.getGraveyard(), target, source, game); + Card card = game.getCard(target.getFirstTarget()); + return card != null && player.moveCards( + card, Zone.BATTLEFIELD, source, game, true, + false, false, null + ); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java index b6ce0a14269..b968e03fbf5 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstormCommander.java @@ -1,6 +1,7 @@ package mage.sets; import mage.cards.ExpansionSet; +import mage.constants.Rarity; import mage.constants.SetType; /** @@ -17,5 +18,7 @@ public final class TarkirDragonstormCommander extends ExpansionSet { private TarkirDragonstormCommander() { super("Tarkir: Dragonstorm Commander", "TDC", ExpansionSet.buildDate(2025, 4, 11), SetType.SUPPLEMENTAL); this.hasBasicLands = false; + + cards.add(new SetCardInfo("Teval, the Balanced Scale", 8, Rarity.MYTHIC, mage.cards.t.TevalTheBalancedScale.class)); } } diff --git a/Mage/src/main/java/mage/game/permanent/token/ZombieDruidToken.java b/Mage/src/main/java/mage/game/permanent/token/ZombieDruidToken.java new file mode 100644 index 00000000000..2081ed4f207 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/ZombieDruidToken.java @@ -0,0 +1,30 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * @author TheElk801 + */ +public final class ZombieDruidToken extends TokenImpl { + + public ZombieDruidToken() { + super("Zombie Druid Token", "2/2 black Zombie Druid creature token"); + cardType.add(CardType.CREATURE); + color.setBlack(true); + subtype.add(SubType.ZOMBIE); + subtype.add(SubType.DRUID); + power = new MageInt(2); + toughness = new MageInt(2); + } + + private ZombieDruidToken(final ZombieDruidToken token) { + super(token); + } + + @Override + public ZombieDruidToken copy() { + return new ZombieDruidToken(this); + } +} From 0a84f647c9d6ebef45e9616d640ffec41b4b9c51 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 13:03:15 -0500 Subject: [PATCH 37/69] fix verify failure (will still fail until mtgjson updates) --- Mage.Sets/src/mage/sets/TarkirDragonstorm.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 2b48b2570c0..8edacaaf63e 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -18,6 +18,7 @@ public final class TarkirDragonstorm extends ExpansionSet { private TarkirDragonstorm() { super("Tarkir: Dragonstorm", "TDM", ExpansionSet.buildDate(2025, 4, 11), SetType.EXPANSION); this.blockName = "Tarkir: Dragonstorm"; // for sorting in GUI + this.hasBasicLands = false; // temporary cards.add(new SetCardInfo("Mox Jasper", 246, Rarity.MYTHIC, mage.cards.m.MoxJasper.class)); cards.add(new SetCardInfo("Skirmish Rhino", 224, Rarity.UNCOMMON, mage.cards.s.SkirmishRhino.class)); From 158212bbebb56525558619d40584f8ab9ddcbd54 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Sat, 22 Feb 2025 14:57:26 -0600 Subject: [PATCH 38/69] refactor: create common DynamicValue for instant and sorcery spells cast in a turn (#13374) --- Mage.Sets/src/mage/cards/d/Demilich.java | 38 +--------- Mage.Sets/src/mage/cards/l/LockAndLoad.java | 48 +----------- .../src/mage/cards/r/RalLeylineProdigy.java | 42 ++-------- .../src/mage/cards/r/RalMonsoonMage.java | 4 +- .../src/mage/cards/r/RionyaFireDancer.java | 60 +-------------- .../src/mage/cards/s/ShowOfConfidence.java | 14 +--- Mage.Sets/src/mage/cards/s/SorcererClass.java | 42 +--------- .../common/InstantAndSorceryCastThisTurn.java | 76 +++++++++++++++++++ 8 files changed, 103 insertions(+), 221 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/dynamicvalue/common/InstantAndSorceryCastThisTurn.java diff --git a/Mage.Sets/src/mage/cards/d/Demilich.java b/Mage.Sets/src/mage/cards/d/Demilich.java index 329903847a3..df95d282491 100644 --- a/Mage.Sets/src/mage/cards/d/Demilich.java +++ b/Mage.Sets/src/mage/cards/d/Demilich.java @@ -10,21 +10,17 @@ import mage.abilities.costs.Costs; import mage.abilities.costs.CostsImpl; import mage.abilities.costs.common.ExileFromGraveCost; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.Effect; import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; -import mage.abilities.hint.ValueHint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.stack.Spell; import mage.players.Player; import mage.target.common.TargetCardInYourGraveyard; -import mage.watchers.common.SpellsCastWatcher; import java.util.UUID; @@ -43,8 +39,8 @@ public final class Demilich extends CardImpl { // This spell costs {U} less to cast for each instant and sorcery you've cast this turn. this.addAbility(new SimpleStaticAbility(Zone.ALL, new SpellCostReductionForEachSourceEffect( - new ManaCostsImpl<>("{U}"), DemilichValue.instance - )).addHint(new ValueHint("Instants and sorceries you've cast this turn", DemilichValue.instance))); + new ManaCostsImpl<>("{U}"), InstantAndSorceryCastThisTurn.YOU + )).addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); // Whenever Demilich attacks, exile up to one target instant or sorcery card from your graveyard. Copy it. You may cast the copy. Ability ability = new AttacksTriggeredAbility(new ExileTargetCardCopyAndCastEffect(false).setText( @@ -66,34 +62,6 @@ public final class Demilich extends CardImpl { } } -enum DemilichValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - int spells = 0; - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher != null) { - for (Spell spell : watcher.getSpellsCastThisTurn(sourceAbility.getControllerId())) { - if (spell.isInstantOrSorcery(game)) { - spells++; - } - } - } - return spells; - } - - @Override - public DemilichValue copy() { - return instance; - } - - @Override - public String getMessage() { - return "instant and sorcery spell you've cast this turn"; - } -} - class DemilichPlayEffect extends AsThoughEffectImpl { DemilichPlayEffect() { diff --git a/Mage.Sets/src/mage/cards/l/LockAndLoad.java b/Mage.Sets/src/mage/cards/l/LockAndLoad.java index c249aec13ac..0d92c96e37a 100644 --- a/Mage.Sets/src/mage/cards/l/LockAndLoad.java +++ b/Mage.Sets/src/mage/cards/l/LockAndLoad.java @@ -1,18 +1,12 @@ package mage.cards.l; -import mage.abilities.Ability; -import mage.abilities.dynamicvalue.DynamicValue; -import mage.abilities.dynamicvalue.IntPlusDynamicValue; -import mage.abilities.effects.Effect; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.keyword.PlotAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.game.Game; -import mage.watchers.common.SpellsCastWatcher; -import java.util.Objects; import java.util.UUID; /** @@ -20,15 +14,14 @@ import java.util.UUID; */ public final class LockAndLoad extends CardImpl { - private static final DynamicValue xValue = new IntPlusDynamicValue(1, LockAndLoadValue.instance); - public LockAndLoad(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{U}"); // Draw a card, then draw a card for each other instant and sorcery spell you've cast this turn. - this.getSpellAbility().addEffect(new DrawCardSourceControllerEffect(xValue) + this.getSpellAbility() + .addHint(InstantAndSorceryCastThisTurn.YOU.getHint()) + .addEffect(new DrawCardSourceControllerEffect(InstantAndSorceryCastThisTurn.YOU) .setText("Draw a card, then draw a card for each other instant and sorcery spell you've cast this turn")); - // Plot {3}{U} this.addAbility(new PlotAbility("{3}{U}")); } @@ -41,37 +34,4 @@ public final class LockAndLoad extends CardImpl { public LockAndLoad copy() { return new LockAndLoad(this); } -} - -enum LockAndLoadValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - return watcher == null ? 0 : - watcher.getSpellsCastThisTurn(sourceAbility.getControllerId()) - .stream() - .filter(Objects::nonNull) - .filter(s -> s.isInstantOrSorcery(game)) - .filter(s -> !s.getSourceId().equals(sourceAbility.getSourceId()) - || s.getZoneChangeCounter(game) != sourceAbility.getSourceObjectZoneChangeCounter()) - .mapToInt(x -> 1) - .sum(); - } - - @Override - public LockAndLoadValue copy() { - return this; - } - - @Override - public String toString() { - return "X"; - } - - @Override - public String getMessage() { - return "Number of other instant and sorcery spell you've cast this turn"; - } } \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/r/RalLeylineProdigy.java b/Mage.Sets/src/mage/cards/r/RalLeylineProdigy.java index d5bd0c3d84c..a900ac858f3 100644 --- a/Mage.Sets/src/mage/cards/r/RalLeylineProdigy.java +++ b/Mage.Sets/src/mage/cards/r/RalLeylineProdigy.java @@ -10,9 +10,8 @@ import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.condition.Condition; import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; import mage.abilities.decorator.ConditionalOneShotEffect; -import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.AsThoughEffectImpl; -import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DamageMultiEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; @@ -34,9 +33,7 @@ import mage.game.Game; import mage.players.Player; import mage.target.common.TargetAnyTargetAmount; import mage.util.CardUtil; -import mage.watchers.common.SpellsCastWatcher; -import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -67,9 +64,11 @@ public final class RalLeylineProdigy extends CardImpl { // Ral, Leyline Prodigy enters the battlefield with an additional loyalty counter on him for each instant and sorcery spell you've cast this turn. this.addAbility(new EntersBattlefieldAbility( - new AddCountersSourceEffect(CounterType.LOYALTY.createInstance(), RalLeylineProdigyValue.instance, false) - .setText("with an additional loyalty counter on him for each instant and sorcery spell you've cast this turn") - )); + new AddCountersSourceEffect(CounterType.LOYALTY.createInstance(), InstantAndSorceryCastThisTurn.YOU, + false) + .setText("with an additional loyalty counter on him for each instant and sorcery spell you've cast this turn")) + .addHint(InstantAndSorceryCastThisTurn.YOU.getHint()) + ); // +1: Until your next turn, instant and sorcery spells you cast cost {1} less to cast. this.addAbility(new LoyaltyAbility(new RalLeylineProdigyCostReductionEffect(), 1)); @@ -126,35 +125,6 @@ class RalLeylineProdigyCostReductionEffect extends OneShotEffect { } } -enum RalLeylineProdigyValue implements DynamicValue { - instance; - - @Override - public int calculate(Game game, Ability sourceAbility, Effect effect) { - SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); - if (watcher == null) { - return 0; - } - return watcher - .getSpellsCastThisTurn(sourceAbility.getControllerId()) - .stream() - .filter(Objects::nonNull) - .filter(spell -> spell.isInstantOrSorcery(game)) - .mapToInt(spell -> 1) - .sum(); - } - - @Override - public RalLeylineProdigyValue copy() { - return instance; - } - - @Override - public String getMessage() { - return "instant and sorcery spell you've cast this turn"; - } -} - class RalLeylineProdigyMinusEightEffect extends OneShotEffect { RalLeylineProdigyMinusEightEffect() { diff --git a/Mage.Sets/src/mage/cards/r/RalMonsoonMage.java b/Mage.Sets/src/mage/cards/r/RalMonsoonMage.java index 01d6a767f89..c579098b09c 100644 --- a/Mage.Sets/src/mage/cards/r/RalMonsoonMage.java +++ b/Mage.Sets/src/mage/cards/r/RalMonsoonMage.java @@ -2,6 +2,7 @@ package mage.cards.r; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.constants.Pronoun; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility; @@ -45,7 +46,8 @@ public final class RalMonsoonMage extends CardImpl { // Whenever you cast an instant or sorcery spell during your turn, flip a coin. If you lose the flip, Ral, Monsoon Mage deals 1 damage to you. If you win the flip, you may exile Ral. If you do, return him to the battlefield transformed under his owner control. this.addAbility(new TransformAbility()); - this.addAbility(new RalMonsoonMageTriggeredAbility()); + this.addAbility(new RalMonsoonMageTriggeredAbility() + .addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); } private RalMonsoonMage(final RalMonsoonMage card) { diff --git a/Mage.Sets/src/mage/cards/r/RionyaFireDancer.java b/Mage.Sets/src/mage/cards/r/RionyaFireDancer.java index f60d70ea09a..5dc76c5a02c 100644 --- a/Mage.Sets/src/mage/cards/r/RionyaFireDancer.java +++ b/Mage.Sets/src/mage/cards/r/RionyaFireDancer.java @@ -2,23 +2,17 @@ package mage.cards.r; import mage.MageInt; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; -import mage.abilities.hint.Hint; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.events.GameEvent; -import mage.game.stack.Spell; import mage.target.TargetPermanent; -import mage.util.CardUtil; -import mage.watchers.Watcher; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -40,7 +34,7 @@ public final class RionyaFireDancer extends CardImpl { new RionyaFireDancerEffect() ); ability.addTarget(new TargetPermanent(StaticFilters.FILTER_ANOTHER_CREATURE_YOU_CONTROL)); - this.addAbility(ability.addHint(RionyaFireDancerHint.instance), new RionyaFireDancerWatcher()); + this.addAbility(ability.addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); } private RionyaFireDancer(final RionyaFireDancer card) { @@ -53,21 +47,6 @@ public final class RionyaFireDancer extends CardImpl { } } -enum RionyaFireDancerHint implements Hint { - instance; - - @Override - public String getText(Game game, Ability ability) { - return "Instants and sorceries you've cast this turn: " - + RionyaFireDancerWatcher.getValue(ability.getControllerId(), game); - } - - @Override - public RionyaFireDancerHint copy() { - return instance; - } -} - class RionyaFireDancerEffect extends OneShotEffect { RionyaFireDancerEffect() { @@ -90,41 +69,10 @@ class RionyaFireDancerEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { CreateTokenCopyTargetEffect effect = new CreateTokenCopyTargetEffect( source.getControllerId(), null, true, - RionyaFireDancerWatcher.getValue(source.getControllerId(), game) + 1 + InstantAndSorceryCastThisTurn.YOU.calculate(game, source, this) + 1 ); effect.apply(game, source); effect.exileTokensCreatedAtNextEndStep(game, source); return true; } -} - -class RionyaFireDancerWatcher extends Watcher { - - private final Map playerMap = new HashMap<>(); - - RionyaFireDancerWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (event.getType() != GameEvent.EventType.SPELL_CAST) { - return; - } - Spell spell = game.getSpell(event.getTargetId()); - if (spell != null && spell.isInstantOrSorcery(game)) { - playerMap.compute(spell.getControllerId(), CardUtil::setOrIncrementValue); - } - } - - @Override - public void reset() { - super.reset(); - playerMap.clear(); - } - - static int getValue(UUID playerId, Game game) { - RionyaFireDancerWatcher watcher = game.getState().getWatcher(RionyaFireDancerWatcher.class); - return watcher == null ? 0 : watcher.playerMap.getOrDefault(playerId, 0); - } -} +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/s/ShowOfConfidence.java b/Mage.Sets/src/mage/cards/s/ShowOfConfidence.java index 3672f5637ae..a9c501e41e0 100644 --- a/Mage.Sets/src/mage/cards/s/ShowOfConfidence.java +++ b/Mage.Sets/src/mage/cards/s/ShowOfConfidence.java @@ -1,6 +1,7 @@ package mage.cards.s; import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CastSourceTriggeredAbility; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; @@ -17,7 +18,6 @@ import mage.game.stack.Spell; import mage.target.common.TargetCreaturePermanent; import mage.watchers.common.SpellsCastWatcher; -import java.util.Objects; import java.util.UUID; /** @@ -29,7 +29,8 @@ public final class ShowOfConfidence extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}"); // When you cast this spell, copy it for each other instant or sorcery spell you've cast this turn. You may choose new targets for the copies. - this.addAbility(new CastSourceTriggeredAbility(new ShowOfConfidenceEffect())); + this.addAbility(new CastSourceTriggeredAbility(new ShowOfConfidenceEffect()) + .addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); // Put a +1/+1 counter on target creature. It gains vigilance until end of turn. this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance())); @@ -73,14 +74,7 @@ class ShowOfConfidenceEffect extends OneShotEffect { if (spell == null || watcher == null) { return false; } - int copies = watcher.getSpellsCastThisTurn(source.getControllerId()) - .stream() - .filter(Objects::nonNull) - .filter(spell1 -> spell1.isInstantOrSorcery(game)) - .filter(s -> !s.getSourceId().equals(source.getSourceId()) - || s.getZoneChangeCounter(game) != source.getSourceObjectZoneChangeCounter()) - .mapToInt(x -> 1) - .sum(); + int copies = InstantAndSorceryCastThisTurn.YOU.calculate(game, source, this) - 1; if (copies > 0) { spell.createCopyOnStack(game, source, source.getControllerId(), true, copies); } diff --git a/Mage.Sets/src/mage/cards/s/SorcererClass.java b/Mage.Sets/src/mage/cards/s/SorcererClass.java index e46d532f5a8..b1d333628b6 100644 --- a/Mage.Sets/src/mage/cards/s/SorcererClass.java +++ b/Mage.Sets/src/mage/cards/s/SorcererClass.java @@ -10,6 +10,7 @@ import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.SpellCastControllerTriggeredAbility; import mage.abilities.condition.Condition; import mage.abilities.costs.Cost; +import mage.abilities.dynamicvalue.common.InstantAndSorceryCastThisTurn; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DrawDiscardControllerEffect; import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; @@ -24,14 +25,9 @@ import mage.cards.CardSetInfo; import mage.constants.*; import mage.filter.StaticFilters; import mage.game.Game; -import mage.game.events.GameEvent; import mage.game.stack.Spell; import mage.players.Player; -import mage.util.CardUtil; -import mage.watchers.Watcher; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -76,7 +72,7 @@ public final class SorcererClass extends CardImpl { StaticFilters.FILTER_SPELL_AN_INSTANT_OR_SORCERY, false, SetTargetPointer.SPELL ), 3 - )), new SorcererClassWatcher()); + )).addHint(InstantAndSorceryCastThisTurn.YOU.getHint())); } private SorcererClass(final SorcererClass card) { @@ -151,7 +147,7 @@ class SorcererClassEffect extends OneShotEffect { if (spell == null) { return false; } - int count = SorcererClassWatcher.spellCount(source.getControllerId(), game); + int count = InstantAndSorceryCastThisTurn.YOU.calculate(game, source, this); if (count < 1) { return false; } @@ -165,35 +161,3 @@ class SorcererClassEffect extends OneShotEffect { return true; } } - -class SorcererClassWatcher extends Watcher { - - private final Map spellMap = new HashMap<>(); - - SorcererClassWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - if (event.getType() != GameEvent.EventType.SPELL_CAST) { - return; - } - Spell spell = game.getSpell(event.getTargetId()); - if (spell == null || !spell.isInstantOrSorcery(game)) { - return; - } - spellMap.compute(spell.getControllerId(), CardUtil::setOrIncrementValue); - } - - @Override - public void reset() { - spellMap.clear(); - super.reset(); - } - - static int spellCount(UUID playerId, Game game) { - SorcererClassWatcher watcher = game.getState().getWatcher(SorcererClassWatcher.class); - return watcher != null ? watcher.spellMap.getOrDefault(playerId, 0) : 0; - } -} diff --git a/Mage/src/main/java/mage/abilities/dynamicvalue/common/InstantAndSorceryCastThisTurn.java b/Mage/src/main/java/mage/abilities/dynamicvalue/common/InstantAndSorceryCastThisTurn.java new file mode 100644 index 00000000000..6ffe2ace4b0 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/dynamicvalue/common/InstantAndSorceryCastThisTurn.java @@ -0,0 +1,76 @@ +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.abilities.hint.ValueHint; +import mage.game.Game; +import mage.watchers.common.SpellsCastWatcher; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.UUID; + +public enum InstantAndSorceryCastThisTurn implements DynamicValue +{ + YOU("you've cast"), + ALL("all players have cast"), + OPPONENTS("your opponents have cast"); + + private final String message; + private final ValueHint hint; + + InstantAndSorceryCastThisTurn(String message) { + this.message = "Instant and sorcery spells " + message + " this turn"; + this.hint = new ValueHint(this.message, this); + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return getSpellsCastThisTurn(game, sourceAbility); + } + + @Override + public InstantAndSorceryCastThisTurn copy() { + return this; + } + + @Override + public String getMessage() { + return this.message; + } + + public Hint getHint() { + return this.hint; + } + + private int getSpellsCastThisTurn(Game game, Ability ability) { + Collection playerIds; + switch (this) { + case YOU: + playerIds = Collections.singletonList(ability.getControllerId()); + break; + case ALL: + playerIds = game.getState().getPlayersInRange(ability.getControllerId(), game); + break; + case OPPONENTS: + playerIds = game.getOpponents(ability.getControllerId()); + break; + default: + return 0; + } + SpellsCastWatcher watcher = game.getState().getWatcher(SpellsCastWatcher.class); + if (watcher == null) { + return 0; + } + return (int) playerIds.stream() + .map(watcher::getSpellsCastThisTurn) + .flatMap(Collection::stream) + .filter(Objects::nonNull) + .filter(spell -> spell.isInstantOrSorcery(game)) + .count(); + } +} \ No newline at end of file From b47f07782948f840014b1390a8c26019c8f7b881 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 13:05:42 -0500 Subject: [PATCH 39/69] [FIN] update spoiler --- Utils/mtg-cards-data.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index edc9a529505..66151efc0c2 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -57181,15 +57181,15 @@ Cloud, Ex-SOLDIER|Final Fantasy Commander|2|M|{2}{R}{G}{W}|Legendary Creature - Terra, Herald of Hope|Final Fantasy Commander|4|M|{R}{W}{B}|Legendary Creature - Human Wizard Warrior|3|3|Trance -- At the beginning of combat on your turn, mill two cards. Terra gains flying until end of turn.$Whenever Terra deals combat damage to a player, you may pay {2}. When you do, return target creature card with power 3 or less from your graveyard to the battlefield tapped.| Tidus, Yuna's Guardian|Final Fantasy Commander|5|M|{G}{W}{U}|Legendary Creature - Human Warrior|3|3|At the beginning of combat on your turn, you may move a counter from target creature you control onto a second target creature you control.$Cheer - Whenever one or more creatures you control with counters on them deal combat damage to a player, you may draw a card and proliferate. Do this only once each turn.| Y'shtola, Night's Blessed|Final Fantasy Commander|7|M|{1}{W}{U}{B}|Legendary Creature - Cat Warlock|2|4|Vigilance$At the beginning of each end step, if a player lost 4 or more life this turn, you draw a card.$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.| -Sidequest: Catch a Fish|Final Fantasy|31|U|{2}{W}|Enchantment|||At the beginning of your upkeep, look at the top card of your libraray. If it's an aartifact or creature card, you may reveal it and put it into your hand. If you put a card into your hand this way, create a Food token and transform this enchantment.| +Sidequest: Catch a Fish|Final Fantasy|31|U|{2}{W}|Enchantment|||At the beginning of your upkeep, look at the top card of your library. If it's an artifact or creature card, you may reveal it and put it into your hand. If you put a card into your hand this way, create a Food token and transform this enchantment.| Cooking Campsite|Final Fantasy|31|U||Land|||{T}: Add {W}.${3}, {T}, Sacrifice an artifact: Put a +1/+1 counter on each creature you control. Activate only as a sorcery.| -Stiltzkin, Moogle Merchant|Final Fantasy|34|R|{W}|Legendary Creature Moogle|1|2|Lifelink${2}, {T}: Target opponent gains control of another target permanent you control. If they do, you draw a card.| -Summon: Shiva|Final Fantasy|78|U|{3}{U}{U}|Enchantment Creature - Saga Elemental|4|5|(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)$Heavenly Strike -- Tap target creature an opponent controls. Put a stun counter it.$Diamond Dust -- Draw a card for each tapped creature your opponents control.| +Stiltzkin, Moogle Merchant|Final Fantasy|34|R|{W}|Legendary Creature - Moogle|1|2|Lifelink${2}, {T}: Target opponent gains control of another target permanent you control. If they do, you draw a card.| +Summon: Shiva|Final Fantasy|78|U|{3}{U}{U}|Enchantment Creature - Saga Elemental|4|5|(As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.)$I, II -- Heavenly Strike -- Tap target creature an opponent controls. Put a stun counter on it.$III -- Diamond Dust -- Draw a card for each tapped creature your opponents control.| Cecil, Dark Knight|Final Fantasy|91|R|{B}|Legendary Creature - Human Knight|2|3|Deathtouch$Darkness -- Whenever Cecil deals damage, you lose that much life. Then if your life total is less than or equal to half your starting life total, untap Cecil and transform it.| -Cecil, Redeemed Paladin|Final Fantasy|91|R||Legendary Creature - Human Knight|4|4|Lifelink$Protect Whenever Cecil attacks, other attacking creatures gain indestructible until end of turn.| +Cecil, Redeemed Paladin|Final Fantasy|91|R||Legendary Creature - Human Knight|4|4|Lifelink$Protect -- Whenever Cecil attacks, other attacking creatures gain indestructible until end of turn.| Tonberry|Final Fantasy|122|U|{B}|Creature - Salamander Horror|2|1|This creature enters tapped with a stun counter on it.$Chef's Knife -- During your turn, this creature has first strike and deathtouch.| Jumbo Cactuar|Final Fantasy|191|R|{5}{G}{G}|Creature - Plant|1|7|10,000 Needles -- Whenever this creature attacks, it gets +9999/+0 until end of turn.| -Sazh's Chocobo|Final Fantasy|200|U|{G}|Creature - Bird|0|1|Landfall -- Whenever a land you control enters, put a+1/+1 counter on this creature.| +Sazh's Chocobo|Final Fantasy|200|U|{G}|Creature - Bird|0|1|Landfall -- Whenever a land you control enters, put a +1/+1 counter on this creature.| Emet-Selch, Unsundered|Final Fantasy|218|M|{1}{U}{B}|Legendary Creature - Elder Wizard|2|4|Vigilance$Whenever Emet-Selch enters or attacks, draw a card, then discard a card.$At the beginning of your upkeep, if there are fourteen or more cards in your graveyard, you may transform Emet-Selch.| Hades, Sorcerer of Eld|Final Fantasy|218|M||Legendary Creature - Avatar|6|6|Vigilance$Echo of the Lost -- During your turn, you may play cards from your graveyard.$If a card or token would be put into your graveyard from anywhere, exile it instead.| Garland, Knight of Cornelia|Final Fantasy|221|U|{B}{R}|Legendary Creature - Human Knight|3|2|Whenever you cast a noncreature spell, surveil 1.${3}{B}{B}{R}{R}: Return this card from your graveyard to the battlefield transformed. Activate only as a sorcery.| From a250f1be979de5e6efffe7d1fbbc131e96910155 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 13:20:03 -0500 Subject: [PATCH 40/69] [FIN] Implement Summon: Shiva --- Mage.Sets/src/mage/cards/s/SummonShiva.java | 76 +++++++++++++++++++++ Mage.Sets/src/mage/sets/FinalFantasy.java | 1 + 2 files changed, 77 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SummonShiva.java diff --git a/Mage.Sets/src/mage/cards/s/SummonShiva.java b/Mage.Sets/src/mage/cards/s/SummonShiva.java new file mode 100644 index 00000000000..0a4406a89c9 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SummonShiva.java @@ -0,0 +1,76 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.SagaAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.TapTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SagaChapter; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterOpponentsCreaturePermanent; +import mage.filter.predicate.permanent.TappedPredicate; +import mage.target.common.TargetOpponentsCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SummonShiva extends CardImpl { + + private static final FilterPermanent filter + = new FilterOpponentsCreaturePermanent("tapped creature your opponents control"); + + static { + filter.add(TappedPredicate.TAPPED); + } + + private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(filter, 1); + private static final Hint hint = new ValueHint("Tapped creatures your opponents control", xValue); + + public SummonShiva(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT, CardType.CREATURE}, "{3}{U}{U}"); + + this.subtype.add(SubType.SAGA); + this.subtype.add(SubType.ELEMENTAL); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // (As this Saga enters and after your draw step, add a lore counter. Sacrifice after III.) + SagaAbility sagaAbility = new SagaAbility(this); + + // I, II -- Heavenly Strike -- Tap target creature an opponent controls. Put a stun counter on it. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_I, SagaChapter.CHAPTER_II, ability -> { + ability.addEffect(new TapTargetEffect()); + ability.addEffect(new AddCountersTargetEffect(CounterType.STUN.createInstance()) + .setText("Put a stun counter on it")); + ability.addTarget(new TargetOpponentsCreaturePermanent()); + ability.withFlavorWord("Heavenly Strike"); + }); + + // III -- Diamond Dust -- Draw a card for each tapped creature your opponents control. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_III, ability -> { + ability.addEffect(new DrawCardSourceControllerEffect(xValue)); + ability.withFlavorWord("Diamond Dust"); + ability.addHint(hint); + }); + } + + private SummonShiva(final SummonShiva card) { + super(card); + } + + @Override + public SummonShiva copy() { + return new SummonShiva(this); + } +} diff --git a/Mage.Sets/src/mage/sets/FinalFantasy.java b/Mage.Sets/src/mage/sets/FinalFantasy.java index b7670eac000..4d5518df936 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasy.java +++ b/Mage.Sets/src/mage/sets/FinalFantasy.java @@ -22,5 +22,6 @@ public final class FinalFantasy extends ExpansionSet { cards.add(new SetCardInfo("Jumbo Cactuar", 191, Rarity.RARE, mage.cards.j.JumboCactuar.class)); cards.add(new SetCardInfo("Sazh's Chocobo", 200, Rarity.UNCOMMON, mage.cards.s.SazhsChocobo.class)); + cards.add(new SetCardInfo("Summon: Shiva", 78, Rarity.UNCOMMON, mage.cards.s.SummonShiva.class)); } } From eb7082dfd8390c811dd96b00839726ffe0d592c5 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 13:25:05 -0500 Subject: [PATCH 41/69] [FIN] Implement Tonberry --- Mage.Sets/src/mage/cards/t/Tonberry.java | 62 +++++++++++++++++++++++ Mage.Sets/src/mage/sets/FinalFantasy.java | 1 + 2 files changed, 63 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/t/Tonberry.java diff --git a/Mage.Sets/src/mage/cards/t/Tonberry.java b/Mage.Sets/src/mage/cards/t/Tonberry.java new file mode 100644 index 00000000000..67b858b5578 --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/Tonberry.java @@ -0,0 +1,62 @@ +package mage.cards.t; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.MyTurnCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.TapSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.DeathtouchAbility; +import mage.abilities.keyword.FirstStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class Tonberry extends CardImpl { + + public Tonberry(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{B}"); + + this.subtype.add(SubType.SALAMANDER); + this.subtype.add(SubType.HORROR); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // This creature enters tapped with a stun counter on it. + Ability ability = new EntersBattlefieldAbility( + new TapSourceEffect(true), "tapped with a stun counter on it" + ); + ability.addEffect(new AddCountersSourceEffect(CounterType.STUN.createInstance())); + this.addAbility(ability); + + // Chef's Knife -- During your turn, this creature has first strike and deathtouch. + ability = new SimpleStaticAbility(new ConditionalContinuousEffect( + new GainAbilitySourceEffect(FirstStrikeAbility.getInstance()), + MyTurnCondition.instance, "during your turn, this creature has first strike" + )); + ability.addEffect(new ConditionalContinuousEffect( + new GainAbilitySourceEffect(DeathtouchAbility.getInstance()), + MyTurnCondition.instance, "and deathtouch" + )); + this.addAbility(ability.withFlavorWord("Chef's Knife")); + } + + private Tonberry(final Tonberry card) { + super(card); + } + + @Override + public Tonberry copy() { + return new Tonberry(this); + } +} diff --git a/Mage.Sets/src/mage/sets/FinalFantasy.java b/Mage.Sets/src/mage/sets/FinalFantasy.java index 4d5518df936..4697a47d9a0 100644 --- a/Mage.Sets/src/mage/sets/FinalFantasy.java +++ b/Mage.Sets/src/mage/sets/FinalFantasy.java @@ -23,5 +23,6 @@ public final class FinalFantasy extends ExpansionSet { cards.add(new SetCardInfo("Jumbo Cactuar", 191, Rarity.RARE, mage.cards.j.JumboCactuar.class)); cards.add(new SetCardInfo("Sazh's Chocobo", 200, Rarity.UNCOMMON, mage.cards.s.SazhsChocobo.class)); cards.add(new SetCardInfo("Summon: Shiva", 78, Rarity.UNCOMMON, mage.cards.s.SummonShiva.class)); + cards.add(new SetCardInfo("Tonberry", 122, Rarity.UNCOMMON, mage.cards.t.Tonberry.class)); } } From ed85cf7111680e724554110d143ffbe4ad24314c Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 22 Feb 2025 19:46:38 -0500 Subject: [PATCH 42/69] fix verify failure --- Mage.Sets/src/mage/cards/s/SummonShiva.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage.Sets/src/mage/cards/s/SummonShiva.java b/Mage.Sets/src/mage/cards/s/SummonShiva.java index 0a4406a89c9..aacbe4fdadc 100644 --- a/Mage.Sets/src/mage/cards/s/SummonShiva.java +++ b/Mage.Sets/src/mage/cards/s/SummonShiva.java @@ -63,6 +63,8 @@ public final class SummonShiva extends CardImpl { ability.withFlavorWord("Diamond Dust"); ability.addHint(hint); }); + + this.addAbility(sagaAbility); } private SummonShiva(final SummonShiva card) { From 41f8a475299614cd026f2dc6fd231072c40fd4e7 Mon Sep 17 00:00:00 2001 From: spjspj Date: Sun, 23 Feb 2025 22:46:39 +1100 Subject: [PATCH 43/69] Add in modify_pdf.pl Way to decode all the bits and pieces in a PDF. FlateDecode streams are decoded and then the <414243>Tj style bits of text are decoded. There are also (abc)Tj bits but I don't handle these. (41h=65='A') --- modify_pdf.pl | 156 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 modify_pdf.pl diff --git a/modify_pdf.pl b/modify_pdf.pl new file mode 100644 index 00000000000..978fb2b0449 --- /dev/null +++ b/modify_pdf.pl @@ -0,0 +1,156 @@ +#!/usr/bin/perl + +use 5.010; +use Compress::Raw::Zlib; +use Digest::MD5 qw(md5 md5_hex md5_base64); +use File::Copy; +use LWP::Simple; +use MIME::Base64 qw(encode_base64url decode_base64url); +use POSIX qw(strftime); +use Socket; +use bytes; +use strict; +use warnings; + +my ($infile, $outfile) = @ARGV; +die "Usage: $0 INFILE OUTFILE\n" if not $outfile; + +open my $in, '<', $infile or die; +binmode $in; + +my $cont = ''; + +while (1) +{ + my $success = read $in, $cont, 100, length ($cont); + die $! if not defined $success; + last if not $success; +} +close $in; + +open my $out, '>', $outfile or die; +binmode $out; +print $out $cont; +close $out; + +# Decode the Tj components of the streams +sub hex_val +{ + my $v = $_ [0]; + + if ($v =~ m/\d/) { return $v; } + if ($v eq "a") { return 10; } + if ($v eq "b") { return 11; } + if ($v eq "c") { return 12; } + if ($v eq "d") { return 13; } + if ($v eq "e") { return 14; } + if ($v eq "f") { return 15; } + return $v; +} + +my $overall_text; +my $tj; +my $this_tj; +sub print_str +{ + my $str = $_ [0]; + my $orig_str = $str; + my $this_s; + + while ($str =~ s/(.)(.)//) + { + my $a = hex_val ($1) * 16; + my $b = hex_val ($2); + #print ($a+$b, " "); + $this_s .= chr ($a+$b); + } + + if ($this_s =~ m/ +[A-Z]/) + { + $this_s .= " $this_tj$orig_str<<\n"; + $this_tj = " >> TJ="; + } + else + { + $this_tj .= "\n str:($this_s) $orig_str :"; + } + return $this_s; +} + +sub get_pdf_text +{ + my $text = $_ [0]; + # hh hh hh hh hh << hex based on two + while ($text =~ s/^(.*)\n//im) + { + my $line = $1; + if ($line =~ m/<([0-9a-f]+)>.*?Tj/) + { + my $str = $1; + my $this_s = print_str ($str); + $overall_text .= $this_s; + } + } +} +# Done - Decode the Tj components of the streams + +# Write out the chunks of stream?? +my $keep = 1; +my $cont2 = $cont; +my $keep_cont2 = $cont2; + +my $stream_r = qr/^.*?FlateDecode.*?[^d]stream/s; +my $endstream_after_r = qr/endstream.*/s; +my $endstream_before_r = qr/^.*?endstream/s; +my $newline = qr/\r\n/s; +my $o; + +# MAIN +while ($keep) +{ + my $cont_two = 1; + while ($cont2 =~ m/[^d]stream/im) + { + $cont2 =~ s/$stream_r//; + $keep_cont2 = $cont2; + print ("\n >>> " . length ($cont2)); + $cont2 =~ s/$endstream_after_r//; + print ("\n 2>>> " . length ($cont2)); + $cont2 =~ s/$newline//img; + $keep_cont2 =~ s/$endstream_before_r//; + print ("\n 3>>> " . length ($keep_cont2)); + + # Compressed + my $outfile2 = "perl_stream.$keep.zip"; + open my $out, '>', $outfile2 or die; + binmode $out; + print $out $cont2; + close $out; + + # Decompressed + my $d = new Compress::Raw::Zlib::Inflate(); + my $output = $d->inflate ($cont2, $o); + print " xxxx after inflate..>> $output \n"; + my $outfile2 = $outfile . ".$keep.txt"; + open my $out, '>', $outfile2 or die; + binmode $out; + print $out $o; + close $out; + + get_pdf_text ($o); + + $keep++; + $cont2 = $keep_cont2; + } + + print ("$overall_text\n"); + $keep = 0; +} + +$cont =~ s/\W/_/img; +$cont =~ s/___*/_/img; +#print $cont; + +say length($cont); +print ("$infile >> ", -s $infile, "\n"); +print ("$outfile >> ", -s $outfile, "\n"); From b34aafe0c32f2c613f6c4205be45cc4b68525fe2 Mon Sep 17 00:00:00 2001 From: spjspj Date: Sun, 23 Feb 2025 22:49:18 +1100 Subject: [PATCH 44/69] Delete modify_pdf.pl Apologies. --- modify_pdf.pl | 156 -------------------------------------------------- 1 file changed, 156 deletions(-) delete mode 100644 modify_pdf.pl diff --git a/modify_pdf.pl b/modify_pdf.pl deleted file mode 100644 index 978fb2b0449..00000000000 --- a/modify_pdf.pl +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/perl - -use 5.010; -use Compress::Raw::Zlib; -use Digest::MD5 qw(md5 md5_hex md5_base64); -use File::Copy; -use LWP::Simple; -use MIME::Base64 qw(encode_base64url decode_base64url); -use POSIX qw(strftime); -use Socket; -use bytes; -use strict; -use warnings; - -my ($infile, $outfile) = @ARGV; -die "Usage: $0 INFILE OUTFILE\n" if not $outfile; - -open my $in, '<', $infile or die; -binmode $in; - -my $cont = ''; - -while (1) -{ - my $success = read $in, $cont, 100, length ($cont); - die $! if not defined $success; - last if not $success; -} -close $in; - -open my $out, '>', $outfile or die; -binmode $out; -print $out $cont; -close $out; - -# Decode the Tj components of the streams -sub hex_val -{ - my $v = $_ [0]; - - if ($v =~ m/\d/) { return $v; } - if ($v eq "a") { return 10; } - if ($v eq "b") { return 11; } - if ($v eq "c") { return 12; } - if ($v eq "d") { return 13; } - if ($v eq "e") { return 14; } - if ($v eq "f") { return 15; } - return $v; -} - -my $overall_text; -my $tj; -my $this_tj; -sub print_str -{ - my $str = $_ [0]; - my $orig_str = $str; - my $this_s; - - while ($str =~ s/(.)(.)//) - { - my $a = hex_val ($1) * 16; - my $b = hex_val ($2); - #print ($a+$b, " "); - $this_s .= chr ($a+$b); - } - - if ($this_s =~ m/ +[A-Z]/) - { - $this_s .= " $this_tj$orig_str<<\n"; - $this_tj = " >> TJ="; - } - else - { - $this_tj .= "\n str:($this_s) $orig_str :"; - } - return $this_s; -} - -sub get_pdf_text -{ - my $text = $_ [0]; - # hh hh hh hh hh << hex based on two - while ($text =~ s/^(.*)\n//im) - { - my $line = $1; - if ($line =~ m/<([0-9a-f]+)>.*?Tj/) - { - my $str = $1; - my $this_s = print_str ($str); - $overall_text .= $this_s; - } - } -} -# Done - Decode the Tj components of the streams - -# Write out the chunks of stream?? -my $keep = 1; -my $cont2 = $cont; -my $keep_cont2 = $cont2; - -my $stream_r = qr/^.*?FlateDecode.*?[^d]stream/s; -my $endstream_after_r = qr/endstream.*/s; -my $endstream_before_r = qr/^.*?endstream/s; -my $newline = qr/\r\n/s; -my $o; - -# MAIN -while ($keep) -{ - my $cont_two = 1; - while ($cont2 =~ m/[^d]stream/im) - { - $cont2 =~ s/$stream_r//; - $keep_cont2 = $cont2; - print ("\n >>> " . length ($cont2)); - $cont2 =~ s/$endstream_after_r//; - print ("\n 2>>> " . length ($cont2)); - $cont2 =~ s/$newline//img; - $keep_cont2 =~ s/$endstream_before_r//; - print ("\n 3>>> " . length ($keep_cont2)); - - # Compressed - my $outfile2 = "perl_stream.$keep.zip"; - open my $out, '>', $outfile2 or die; - binmode $out; - print $out $cont2; - close $out; - - # Decompressed - my $d = new Compress::Raw::Zlib::Inflate(); - my $output = $d->inflate ($cont2, $o); - print " xxxx after inflate..>> $output \n"; - my $outfile2 = $outfile . ".$keep.txt"; - open my $out, '>', $outfile2 or die; - binmode $out; - print $out $o; - close $out; - - get_pdf_text ($o); - - $keep++; - $cont2 = $keep_cont2; - } - - print ("$overall_text\n"); - $keep = 0; -} - -$cont =~ s/\W/_/img; -$cont =~ s/___*/_/img; -#print $cont; - -say length($cont); -print ("$infile >> ", -s $infile, "\n"); -print ("$outfile >> ", -s $outfile, "\n"); From 55474322e24bfbdf2fd9bf9b0a5b61007891913f Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sun, 23 Feb 2025 10:29:27 -0500 Subject: [PATCH 45/69] [TDM] Implement Inevitable Defeat --- .../src/mage/cards/i/InevitableDefeat.java | 40 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 41 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/i/InevitableDefeat.java diff --git a/Mage.Sets/src/mage/cards/i/InevitableDefeat.java b/Mage.Sets/src/mage/cards/i/InevitableDefeat.java new file mode 100644 index 00000000000..d7bae91c059 --- /dev/null +++ b/Mage.Sets/src/mage/cards/i/InevitableDefeat.java @@ -0,0 +1,40 @@ +package mage.cards.i; + +import mage.abilities.common.CantBeCounteredSourceAbility; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeTargetControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.target.common.TargetNonlandPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class InevitableDefeat extends CardImpl { + + public InevitableDefeat(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{R}{W}{B}"); + + // This spell can't be countered. + this.addAbility(new CantBeCounteredSourceAbility()); + + // Exile target nonland permanent. Its controller loses 3 life and you gain 3 life. + this.getSpellAbility().addEffect(new ExileTargetEffect()); + this.getSpellAbility().addEffect(new LoseLifeTargetControllerEffect(3)); + this.getSpellAbility().addEffect(new GainLifeEffect(3).concatBy("and")); + this.getSpellAbility().addTarget(new TargetNonlandPermanent()); + } + + private InevitableDefeat(final InevitableDefeat card) { + super(card); + } + + @Override + public InevitableDefeat copy() { + return new InevitableDefeat(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 8edacaaf63e..2670b257dfd 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -20,6 +20,7 @@ public final class TarkirDragonstorm extends ExpansionSet { this.blockName = "Tarkir: Dragonstorm"; // for sorting in GUI this.hasBasicLands = false; // temporary + 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("Skirmish Rhino", 224, Rarity.UNCOMMON, mage.cards.s.SkirmishRhino.class)); cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); From 24af0ff56887d310485b56920789180ca23f4750 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sun, 23 Feb 2025 10:35:15 -0500 Subject: [PATCH 46/69] [TDM] Implement Narset, Jeskai Waymaster --- .../mage/cards/n/NarsetJeskaiWaymaster.java | 81 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 82 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/n/NarsetJeskaiWaymaster.java diff --git a/Mage.Sets/src/mage/cards/n/NarsetJeskaiWaymaster.java b/Mage.Sets/src/mage/cards/n/NarsetJeskaiWaymaster.java new file mode 100644 index 00000000000..8525cf229d5 --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NarsetJeskaiWaymaster.java @@ -0,0 +1,81 @@ +package mage.cards.n; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.costs.common.DiscardHandCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DoIfCostPaid; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.hint.Hint; +import mage.abilities.hint.ValueHint; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.game.Game; +import mage.watchers.common.CastSpellLastTurnWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NarsetJeskaiWaymaster extends CardImpl { + + public NarsetJeskaiWaymaster(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{R}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.MONK); + this.power = new MageInt(3); + this.toughness = new MageInt(4); + + // At the beginning of your end step, you may discard your hand. If you do, draw cards equal to the number of spells you've cast this turn. + this.addAbility(new BeginningOfEndStepTriggeredAbility(new DoIfCostPaid( + new DrawCardSourceControllerEffect(NarsetJeskaiWaymasterValue.instance), new DiscardHandCost() + )).addHint(NarsetJeskaiWaymasterValue.getHint())); + } + + private NarsetJeskaiWaymaster(final NarsetJeskaiWaymaster card) { + super(card); + } + + @Override + public NarsetJeskaiWaymaster copy() { + return new NarsetJeskaiWaymaster(this); + } +} + +enum NarsetJeskaiWaymasterValue implements DynamicValue { + instance; + private static final Hint hint = new ValueHint("Spells you've cast this turn", instance); + + public static Hint getHint() { + return hint; + } + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + CastSpellLastTurnWatcher watcher = game.getState().getWatcher(CastSpellLastTurnWatcher.class); + return watcher == null ? 0 : watcher.getAmountOfSpellsPlayerCastOnCurrentTurn(sourceAbility.getControllerId()); + } + + @Override + public NarsetJeskaiWaymasterValue copy() { + return this; + } + + @Override + public String getMessage() { + return "the number of spells you've cast this turn"; + } + + @Override + public String toString() { + return "1"; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 2670b257dfd..72bf414219a 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -22,6 +22,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("Skirmish Rhino", 224, Rarity.UNCOMMON, mage.cards.s.SkirmishRhino.class)); cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); } From 0d16c5fb7261e6d64c57c936d4c3e5daf7abcb73 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sun, 23 Feb 2025 10:46:37 -0500 Subject: [PATCH 47/69] [TDM] Implement Barrensteppe Siege --- .../src/mage/cards/b/BarrensteppeSiege.java | 64 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 65 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java diff --git a/Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java b/Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java new file mode 100644 index 00000000000..5bac6c137fd --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BarrensteppeSiege.java @@ -0,0 +1,64 @@ +package mage.cards.b; + +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CreatureDiedControlledCondition; +import mage.abilities.condition.common.ModeChoiceSourceCondition; +import mage.abilities.decorator.ConditionalTriggeredAbility; +import mage.abilities.effects.common.ChooseModeEffect; +import mage.abilities.effects.common.SacrificeOpponentsEffect; +import mage.abilities.effects.common.counter.AddCountersAllEffect; +import mage.abilities.triggers.BeginningOfEndStepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.counters.CounterType; +import mage.filter.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class BarrensteppeSiege extends CardImpl { + + private static final Condition condition1 = new ModeChoiceSourceCondition("Abzan"); + private static final String rule1 = "&bull Abzan — At the beginning of your end step, " + + "put a +1/+1 counter on each creature you control."; + private static final Condition condition2 = new ModeChoiceSourceCondition("Mardu"); + private static final String rule2 = "&bull Mardu — At the beginning of your end step, " + + "if a creature died under your control this turn, each opponent sacrifices a creature of their choice."; + + public BarrensteppeSiege(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{W}{B}"); + + // As this enchantment enters, choose Abzan or Mardu. + this.addAbility(new EntersBattlefieldAbility( + new ChooseModeEffect("Abzan or Mardu?", "Abzan", "Mardu"), + null, "As {this} enters, choose Abzan or Mardu.", "" + )); + + // * Abzan -- At the beginning of your end step, put a +1/+1 counter on each creature you control. + this.addAbility(new ConditionalTriggeredAbility( + new BeginningOfEndStepTriggeredAbility(new AddCountersAllEffect( + CounterType.P1P1.createInstance(), StaticFilters.FILTER_CONTROLLED_CREATURE + )), condition1, rule1 + )); + + // * Mardu -- At the beginning of your end step, if a creature died under your control this turn, each opponent sacrifices a creature of their choice. + this.addAbility(new ConditionalTriggeredAbility( + new BeginningOfEndStepTriggeredAbility( + new SacrificeOpponentsEffect(StaticFilters.FILTER_PERMANENT_CREATURE) + ).withInterveningIf(CreatureDiedControlledCondition.instance), condition2, rule2 + )); + } + + private BarrensteppeSiege(final BarrensteppeSiege card) { + super(card); + } + + @Override + public BarrensteppeSiege copy() { + return new BarrensteppeSiege(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 72bf414219a..2dabecda639 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -20,6 +20,7 @@ public final class TarkirDragonstorm extends ExpansionSet { this.blockName = "Tarkir: Dragonstorm"; // for sorting in GUI this.hasBasicLands = false; // temporary + cards.add(new SetCardInfo("Barrensteppe Siege", 171, Rarity.RARE, mage.cards.b.BarrensteppeSiege.class)); 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)); From d2e94bbad71646497145fd74763d28f07d187b60 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sun, 23 Feb 2025 10:50:47 -0500 Subject: [PATCH 48/69] [TDM] Implement Shiko, Paragon of the Way --- .../mage/cards/s/ShikoParagonOfTheWay.java | 64 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 65 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/ShikoParagonOfTheWay.java diff --git a/Mage.Sets/src/mage/cards/s/ShikoParagonOfTheWay.java b/Mage.Sets/src/mage/cards/s/ShikoParagonOfTheWay.java new file mode 100644 index 00000000000..d32d7341b9b --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ShikoParagonOfTheWay.java @@ -0,0 +1,64 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.ExileTargetCardCopyAndCastEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.VigilanceAbility; +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.filter.FilterCard; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ShikoParagonOfTheWay extends CardImpl { + + private static final FilterCard filter = new FilterNonlandCard("nonland card with mana value 3 or less"); + + static { + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 4)); + } + + public ShikoParagonOfTheWay(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{R}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SPIRIT); + this.subtype.add(SubType.DRAGON); + this.power = new MageInt(4); + this.toughness = new MageInt(5); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // When Shiko enters, exile target nonland card with mana value 3 or less from your graveyard. Copy it, then you may cast the copy without paying its mana cost. + Ability ability = new EntersBattlefieldTriggeredAbility(new ExileTargetCardCopyAndCastEffect(true) + .setText("exile target nonland card with mana value 3 or less from your graveyard. " + + "Copy it, then you may cast the copy without paying its mana cost")); + ability.addTarget(new TargetCardInYourGraveyard(filter)); + this.addAbility(ability); + } + + private ShikoParagonOfTheWay(final ShikoParagonOfTheWay card) { + super(card); + } + + @Override + public ShikoParagonOfTheWay copy() { + return new ShikoParagonOfTheWay(this); + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 2dabecda639..18620b54efc 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("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("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); } From 38479b248714b74f06d4d27360a391454dc79b4c Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sun, 23 Feb 2025 10:56:34 -0500 Subject: [PATCH 49/69] [TDM] Implement Smile at Death --- Mage.Sets/src/mage/cards/s/SmileAtDeath.java | 91 +++++++++++++++++++ .../src/mage/sets/TarkirDragonstorm.java | 1 + 2 files changed, 92 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SmileAtDeath.java diff --git a/Mage.Sets/src/mage/cards/s/SmileAtDeath.java b/Mage.Sets/src/mage/cards/s/SmileAtDeath.java new file mode 100644 index 00000000000..5cf3eb68a89 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SmileAtDeath.java @@ -0,0 +1,91 @@ +package mage.cards.s; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInYourGraveyard; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SmileAtDeath extends CardImpl { + + private static final FilterCard filter = new FilterCreatureCard("creature cards with power 2 or less"); + + static { + filter.add(new PowerPredicate(ComparisonType.FEWER_THAN, 3)); + } + + public SmileAtDeath(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{W}{W}"); + + // 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. + Ability ability = new BeginningOfUpkeepTriggeredAbility(new SmileAtDeathEffect()); + ability.addTarget(new TargetCardInYourGraveyard(0, 2, filter)); + this.addAbility(ability); + } + + private SmileAtDeath(final SmileAtDeath card) { + super(card); + } + + @Override + public SmileAtDeath copy() { + return new SmileAtDeath(this); + } +} + +class SmileAtDeathEffect extends OneShotEffect { + + SmileAtDeathEffect() { + super(Outcome.Benefit); + staticText = "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"; + } + + private SmileAtDeathEffect(final SmileAtDeathEffect effect) { + super(effect); + } + + @Override + public SmileAtDeathEffect copy() { + return new SmileAtDeathEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(getTargetPointer().getTargets(game, source)); + cards.retainZone(Zone.GRAVEYARD, game); + if (cards.isEmpty()) { + return false; + } + player.moveCards(cards, Zone.BATTLEFIELD, source, game); + for (UUID cardId : cards) { + Optional.ofNullable(cardId) + .map(game::getPermanent) + .ifPresent(permanent -> permanent.addCounters(CounterType.P1P1.createInstance(), source, game)); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java index 18620b54efc..4346ffbc666 100644 --- a/Mage.Sets/src/mage/sets/TarkirDragonstorm.java +++ b/Mage.Sets/src/mage/sets/TarkirDragonstorm.java @@ -26,6 +26,7 @@ public final class TarkirDragonstorm extends ExpansionSet { cards.add(new SetCardInfo("Narset, Jeskai Waymaster", 209, Rarity.RARE, mage.cards.n.NarsetJeskaiWaymaster.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)); cards.add(new SetCardInfo("Stormscale Scion", 123, Rarity.MYTHIC, mage.cards.s.StormscaleScion.class)); } } From bff5c28938738f00e02707cabb6bc7db738b2bf2 Mon Sep 17 00:00:00 2001 From: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com> Date: Sun, 23 Feb 2025 19:18:50 +0000 Subject: [PATCH 50/69] [DRC] Implement Nissa, Worldsoul Speaker --- .../mage/cards/n/NissaWorldsoulSpeaker.java | 99 +++++++++++++++++++ .../src/mage/sets/AetherdriftCommander.java | 2 + 2 files changed, 101 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java diff --git a/Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java b/Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java new file mode 100644 index 00000000000..fc0ce58f3bc --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java @@ -0,0 +1,99 @@ +package mage.cards.n; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.LandfallAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.SourceIsSpellCondition; +import mage.abilities.costs.AlternativeCostSourceAbility; +import mage.abilities.costs.common.PayEnergyCost; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.counter.GetEnergyCountersControllerEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterPermanentCard; +import mage.game.Game; +import mage.players.Player; + +import java.util.UUID; + +/** + * @author PurpleCrowbar + */ +public final class NissaWorldsoulSpeaker extends CardImpl { + + public NissaWorldsoulSpeaker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{G}"); + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.ELF, SubType.DRUID); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Landfall -- Whenever a land you control enters, you get {E}{E}. + this.addAbility(new LandfallAbility( + new GetEnergyCountersControllerEffect(2), false + )); + + // You may pay eight {E} rather than pay the mana cost for permanent spells you cast. + this.addAbility(new SimpleStaticAbility(new NissaWorldsoulSpeakerEffect())); + } + + private NissaWorldsoulSpeaker(final NissaWorldsoulSpeaker card) { + super(card); + } + + @Override + public NissaWorldsoulSpeaker copy() { + return new NissaWorldsoulSpeaker(this); + } +} + +class NissaWorldsoulSpeakerEffect extends ContinuousEffectImpl { + + private static final FilterPermanentCard filter = new FilterPermanentCard(); + + private final AlternativeCostSourceAbility alternativeCastingCostAbility = new AlternativeCostSourceAbility( + new PayEnergyCost(8), SourceIsSpellCondition.instance, null, filter, true + ); + + public NissaWorldsoulSpeakerEffect() { + super(Duration.WhileOnBattlefield, Outcome.Detriment); + staticText = "You may pay eight {E} rather than pay the mana cost of permanent spells you cast"; + } + + private NissaWorldsoulSpeakerEffect(final NissaWorldsoulSpeakerEffect effect) { + super(effect); + } + + @Override + public NissaWorldsoulSpeakerEffect copy() { + return new NissaWorldsoulSpeakerEffect(this); + } + + @Override + public void init(Ability source, Game game, UUID activePlayerId) { + super.init(source, game, activePlayerId); + alternativeCastingCostAbility.setSourceId(source.getSourceId()); + } + + @Override + public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + controller.getAlternativeSourceCosts().add(alternativeCastingCostAbility); + return true; + } + return false; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean hasLayer(Layer layer) { + return layer == Layer.RulesEffects; + } +} diff --git a/Mage.Sets/src/mage/sets/AetherdriftCommander.java b/Mage.Sets/src/mage/sets/AetherdriftCommander.java index 24dcf213775..e7c4cd86c29 100644 --- a/Mage.Sets/src/mage/sets/AetherdriftCommander.java +++ b/Mage.Sets/src/mage/sets/AetherdriftCommander.java @@ -110,6 +110,8 @@ public final class AetherdriftCommander extends ExpansionSet { cards.add(new SetCardInfo("Midnight Reaper", 44, Rarity.RARE, mage.cards.m.MidnightReaper.class)); cards.add(new SetCardInfo("Murderous Rider", 45, Rarity.RARE, mage.cards.m.MurderousRider.class)); cards.add(new SetCardInfo("Never // Return", 96, Rarity.RARE, mage.cards.n.NeverReturn.class)); + cards.add(new SetCardInfo("Nissa, Worldsoul Speaker", 13, Rarity.RARE, mage.cards.n.NissaWorldsoulSpeaker.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Nissa, Worldsoul Speaker", 29, Rarity.RARE, mage.cards.n.NissaWorldsoulSpeaker.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("On Wings of Gold", 5, Rarity.RARE, mage.cards.o.OnWingsOfGold.class)); cards.add(new SetCardInfo("One with the Machine", 80, Rarity.RARE, mage.cards.o.OneWithTheMachine.class)); cards.add(new SetCardInfo("Ornithopter of Paradise", 133, Rarity.COMMON, mage.cards.o.OrnithopterOfParadise.class)); From 6b735bd77689a37faa989f4fd7b3b3cc484afc1a Mon Sep 17 00:00:00 2001 From: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com> Date: Sun, 23 Feb 2025 22:16:27 +0000 Subject: [PATCH 51/69] Amend rules text for Nissa, Worldsoul Speaker --- Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java b/Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java index fc0ce58f3bc..b0a7001f88b 100644 --- a/Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java +++ b/Mage.Sets/src/mage/cards/n/NissaWorldsoulSpeaker.java @@ -59,7 +59,7 @@ class NissaWorldsoulSpeakerEffect extends ContinuousEffectImpl { public NissaWorldsoulSpeakerEffect() { super(Duration.WhileOnBattlefield, Outcome.Detriment); - staticText = "You may pay eight {E} rather than pay the mana cost of permanent spells you cast"; + staticText = "You may pay eight {E} rather than pay the mana cost for permanent spells you cast"; } private NissaWorldsoulSpeakerEffect(final NissaWorldsoulSpeakerEffect effect) { From 6a1cce5b3fd2f6d8edea05200bcee89daf638408 Mon Sep 17 00:00:00 2001 From: im-inuenc Date: Sun, 23 Feb 2025 23:46:05 +0100 Subject: [PATCH 52/69] [WHO] Implement What Must Be Done, fixed that Profound Journey can't target battle (#13377) --------- Co-authored-by: im-inuenc --- .../src/mage/cards/p/ProfoundJourney.java | 13 +-- .../src/mage/cards/w/WhatMustBeDone.java | 110 ++++++++++++++++++ Mage.Sets/src/mage/sets/AssassinsCreed.java | 6 +- 3 files changed, 115 insertions(+), 14 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/w/WhatMustBeDone.java diff --git a/Mage.Sets/src/mage/cards/p/ProfoundJourney.java b/Mage.Sets/src/mage/cards/p/ProfoundJourney.java index e52a841b1d5..fbe064a4bc7 100644 --- a/Mage.Sets/src/mage/cards/p/ProfoundJourney.java +++ b/Mage.Sets/src/mage/cards/p/ProfoundJourney.java @@ -8,7 +8,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.filter.FilterCard; -import mage.filter.predicate.Predicates; +import mage.filter.common.FilterPermanentCard; import mage.target.common.TargetCardInYourGraveyard; /** @@ -17,16 +17,7 @@ import mage.target.common.TargetCardInYourGraveyard; */ public final class ProfoundJourney extends CardImpl { - private static final FilterCard filter = new FilterCard("permanent card from your graveyard"); - - static { - filter.add(Predicates.or( - CardType.ARTIFACT.getPredicate(), - CardType.CREATURE.getPredicate(), - CardType.ENCHANTMENT.getPredicate(), - CardType.LAND.getPredicate(), - CardType.PLANESWALKER.getPredicate())); - } + private static final FilterCard filter = new FilterPermanentCard("permanent card from your graveyard"); public ProfoundJourney(UUID ownerId, CardSetInfo setInfo) { super(ownerId,setInfo,new CardType[]{CardType.SORCERY},"{5}{W}{W}"); diff --git a/Mage.Sets/src/mage/cards/w/WhatMustBeDone.java b/Mage.Sets/src/mage/cards/w/WhatMustBeDone.java new file mode 100644 index 00000000000..62fef04da9e --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WhatMustBeDone.java @@ -0,0 +1,110 @@ +package mage.cards.w; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.Mode; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.DestroyAllEffect; +import mage.abilities.effects.common.InfoEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.counters.CounterType; +import mage.filter.FilterCard; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterPermanentCard; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.HistoricPredicate; +import mage.game.Game; +import mage.game.events.EntersTheBattlefieldEvent; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCardInYourGraveyard; + +/** + * + * @author anonymous + */ +public final class WhatMustBeDone extends CardImpl { + + private static final FilterPermanent filterArtifactsAndCreatures = new FilterPermanent("artifacts and creatures"); + static { + filterArtifactsAndCreatures.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate() + )); + } + + private static final FilterCard filterHistoricPermanentCard = new FilterPermanentCard("historic permanent card from your graveyard"); + static { + filterHistoricPermanentCard.add(HistoricPredicate.instance); + } + + public WhatMustBeDone(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{W}{W}"); + + // Choose one -- + // * Let the World Burn -- Destroy all artifacts and creatures. + this.getSpellAbility().addEffect(new DestroyAllEffect(filterArtifactsAndCreatures)); + this.getSpellAbility().withFirstModeFlavorWord("Let the World Burn"); + + // * Release Juno -- Return target historic permanent card from your graveyard to the battlefield. It enters with two additional +1/+1 counters on it if it's a creature. + this.getSpellAbility().addMode(new Mode( + new WhatMustBeDoneReplacementEffect( + )).addEffect(new ReturnFromGraveyardToBattlefieldTargetEffect() + ).addTarget(new TargetCardInYourGraveyard(filterHistoricPermanentCard) + ).addEffect(new InfoEffect("It enters with two additional +1/+1 counters on it if it's a creature. " + + "(Artifacts, legendaries, and Sagas are historic.)" ) + ).withFlavorWord("Release Juno")); + } + + private WhatMustBeDone(final WhatMustBeDone card) { + super(card); + } + + @Override + public WhatMustBeDone copy() { + return new WhatMustBeDone(this); + } +} + +class WhatMustBeDoneReplacementEffect extends ReplacementEffectImpl { + + WhatMustBeDoneReplacementEffect() { + super(Duration.EndOfStep, Outcome.BoostCreature); + } + + private WhatMustBeDoneReplacementEffect(final WhatMustBeDoneReplacementEffect effect) { + super(effect); + } + + @Override + 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 getTargetPointer().getTargets(game, source).contains(event.getTargetId()); + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + Permanent permanent = ((EntersTheBattlefieldEvent) event).getTarget(); + if (permanent == null || !permanent.isCreature(game)) { + return false; + } + permanent.addCounters(CounterType.P1P1.createInstance(2), source.getControllerId(), source, game, event.getAppliedEffects()); + discard(); + return false; + } + + @Override + public WhatMustBeDoneReplacementEffect copy() { + return new WhatMustBeDoneReplacementEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/AssassinsCreed.java b/Mage.Sets/src/mage/sets/AssassinsCreed.java index 3ffa041e3a9..a0224341c63 100644 --- a/Mage.Sets/src/mage/sets/AssassinsCreed.java +++ b/Mage.Sets/src/mage/sets/AssassinsCreed.java @@ -324,9 +324,9 @@ public final class AssassinsCreed extends ExpansionSet { cards.add(new SetCardInfo("Viewpoint Synchronization", 223, Rarity.UNCOMMON, mage.cards.v.ViewpointSynchronization.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Viewpoint Synchronization", 43, Rarity.UNCOMMON, mage.cards.v.ViewpointSynchronization.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Waterlogged Grove", 116, Rarity.RARE, mage.cards.w.WaterloggedGrove.class)); - //cards.add(new SetCardInfo("What Must Be Done", 11, Rarity.RARE, mage.cards.w.WhatMustBeDone.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("What Must Be Done", 157, Rarity.RARE, mage.cards.w.WhatMustBeDone.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("What Must Be Done", 184, Rarity.RARE, mage.cards.w.WhatMustBeDone.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("What Must Be Done", 11, Rarity.RARE, mage.cards.w.WhatMustBeDone.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("What Must Be Done", 157, Rarity.RARE, mage.cards.w.WhatMustBeDone.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("What Must Be Done", 184, Rarity.RARE, mage.cards.w.WhatMustBeDone.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Yggdrasil, Rebirth Engine", 126, Rarity.MYTHIC, mage.cards.y.YggdrasilRebirthEngine.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Yggdrasil, Rebirth Engine", 264, Rarity.MYTHIC, mage.cards.y.YggdrasilRebirthEngine.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Yggdrasil, Rebirth Engine", 78, Rarity.MYTHIC, mage.cards.y.YggdrasilRebirthEngine.class, NON_FULL_USE_VARIOUS)); From 40b7d4a3af9df587b96fea56bc7753e05c04094a Mon Sep 17 00:00:00 2001 From: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:11:56 +0000 Subject: [PATCH 53/69] [ACR] Implement Haytham Kenway --- Mage.Sets/src/mage/cards/h/HaythamKenway.java | 74 +++++++++++++++++++ Mage.Sets/src/mage/sets/AssassinsCreed.java | 6 +- 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/h/HaythamKenway.java diff --git a/Mage.Sets/src/mage/cards/h/HaythamKenway.java b/Mage.Sets/src/mage/cards/h/HaythamKenway.java new file mode 100644 index 00000000000..d18d4cc70b8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HaythamKenway.java @@ -0,0 +1,74 @@ +package mage.cards.h; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.ExileUntilSourceLeavesEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.ProtectionAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreaturePermanent; +import mage.target.common.TargetCreaturePermanent; +import mage.target.targetadjustment.ForEachOpponentTargetsAdjuster; +import mage.target.targetpointer.EachTargetPointer; + +import java.util.UUID; + +/** + * @author PurpleCrowbar + */ +public final class HaythamKenway extends CardImpl { + + private static final FilterCard filter = new FilterCard("Assassins"); + private static final FilterCreaturePermanent filter2 = new FilterCreaturePermanent(SubType.KNIGHT, "Knights"); + + static { + filter.add(SubType.ASSASSIN.getPredicate()); + } + + public HaythamKenway(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}{U}"); + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN, SubType.KNIGHT); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Protection from Assassins + this.addAbility(new ProtectionAbility(filter)); + + // Other Knights you control get +2/+2 and have protection from Assassins. + Ability ability = new SimpleStaticAbility(new BoostControlledEffect( + 2, 2, Duration.WhileOnBattlefield, filter2, true + )); + ability.addEffect(new GainAbilityControlledEffect( + new ProtectionAbility(filter), Duration.WhileOnBattlefield, filter2, true + ).setText("and have protection from Assassins")); + this.addAbility(ability); + + // When Haytham Kenway enters, for each opponent, exile up to one target creature that player controls until Haytham Kenway leaves the battlefield. + Ability ability2 = new EntersBattlefieldTriggeredAbility(new ExileUntilSourceLeavesEffect() + .setTargetPointer(new EachTargetPointer()) + .setText("for each opponent, exile up to one target creature that player controls until {this} leaves the battlefield") + ); + ability2.addTarget(new TargetCreaturePermanent(0, 1)); + ability2.setTargetAdjuster(new ForEachOpponentTargetsAdjuster()); + this.addAbility(ability2); + } + + private HaythamKenway(final HaythamKenway card) { + super(card); + } + + @Override + public HaythamKenway copy() { + return new HaythamKenway(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AssassinsCreed.java b/Mage.Sets/src/mage/sets/AssassinsCreed.java index a0224341c63..5897aac8c14 100644 --- a/Mage.Sets/src/mage/sets/AssassinsCreed.java +++ b/Mage.Sets/src/mage/sets/AssassinsCreed.java @@ -157,9 +157,9 @@ public final class AssassinsCreed extends ExpansionSet { //cards.add(new SetCardInfo("Havi, the All-Father", 56, Rarity.RARE, mage.cards.h.HaviTheAllFather.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Haystack", 175, Rarity.UNCOMMON, mage.cards.h.Haystack.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Haystack", 5, Rarity.UNCOMMON, mage.cards.h.Haystack.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Haytham Kenway", 147, Rarity.RARE, mage.cards.h.HaythamKenway.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Haytham Kenway", 238, Rarity.RARE, mage.cards.h.HaythamKenway.class, NON_FULL_USE_VARIOUS)); - //cards.add(new SetCardInfo("Haytham Kenway", 57, Rarity.RARE, mage.cards.h.HaythamKenway.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Haytham Kenway", 57, Rarity.RARE, mage.cards.h.HaythamKenway.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Haytham Kenway", 147, Rarity.RARE, mage.cards.h.HaythamKenway.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Haytham Kenway", 238, Rarity.RARE, mage.cards.h.HaythamKenway.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Headsplitter", 289, Rarity.COMMON, mage.cards.h.Headsplitter.class)); cards.add(new SetCardInfo("Hemlock Vial", 206, Rarity.UNCOMMON, mage.cards.h.HemlockVial.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Hemlock Vial", 26, Rarity.UNCOMMON, mage.cards.h.HemlockVial.class, NON_FULL_USE_VARIOUS)); From 411100e442a4c7e80fedd3bace02e3662762846a Mon Sep 17 00:00:00 2001 From: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com> Date: Tue, 25 Feb 2025 02:23:37 +0000 Subject: [PATCH 54/69] Minor grammar and text fixes --- .../java/mage/client/components/ext/dlg/DialogContainer.java | 2 +- Mage.Server/src/main/java/mage/server/game/GameController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/components/ext/dlg/DialogContainer.java b/Mage.Client/src/main/java/mage/client/components/ext/dlg/DialogContainer.java index f8bdc4ba92e..136546fcfa7 100644 --- a/Mage.Client/src/main/java/mage/client/components/ext/dlg/DialogContainer.java +++ b/Mage.Client/src/main/java/mage/client/components/ext/dlg/DialogContainer.java @@ -81,7 +81,7 @@ public class DialogContainer extends JPanel { case EMBLEMS: { backgroundColor = new Color(0, 0, 50, 110); alpha = 0; - ChoiceDialog dlg = new ChoiceDialog(params, "Command Zone (Commander, Emblems and Planes)"); + ChoiceDialog dlg = new ChoiceDialog(params, "Command Zone (Commanders, Emblems, and Planes)"); add(dlg); dlg.setLocation(X_OFFSET + 10, Y_OFFSET + 10); dlg.updateSize(params.rect.width - 80, params.rect.height - 80); diff --git a/Mage.Server/src/main/java/mage/server/game/GameController.java b/Mage.Server/src/main/java/mage/server/game/GameController.java index 2ffbf7e00b6..96ed8b53505 100644 --- a/Mage.Server/src/main/java/mage/server/game/GameController.java +++ b/Mage.Server/src/main/java/mage/server/game/GameController.java @@ -591,7 +591,7 @@ public class GameController implements GameCallback { if (playerId != null) { Player player = game.getPlayer(playerId); if (player != null) { - game.informPlayers(player.getLogName() + " want to concede"); + game.informPlayers(player.getLogName() + " wants to concede"); game.setConcedingPlayer(getPlayerId(userId)); } } From c2797ee24c8016939d7c7da2b4c69c807803d25a Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Mon, 24 Feb 2025 22:21:46 -0500 Subject: [PATCH 55/69] fix Fanatic of the Harrowing --- Mage.Sets/src/mage/cards/f/FanaticOfTheHarrowing.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/f/FanaticOfTheHarrowing.java b/Mage.Sets/src/mage/cards/f/FanaticOfTheHarrowing.java index dd337f0a4e1..6e921c03f3c 100644 --- a/Mage.Sets/src/mage/cards/f/FanaticOfTheHarrowing.java +++ b/Mage.Sets/src/mage/cards/f/FanaticOfTheHarrowing.java @@ -61,17 +61,21 @@ class FanaticOfTheHarrowingEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { boolean flag = false; for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { - Player player = game.getPlayer(source.getControllerId()); + Player player = game.getPlayer(playerId); if (player == null) { continue; } if (!player.discard(1, false, false, source, game).isEmpty() - && player.equals(source.getControllerId())) { + && playerId.equals(source.getControllerId())) { flag = true; } } + game.processAction(); if (flag) { - game.getPlayer(source.getControllerId()).drawCards(1, source, game); + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + controller.drawCards(1, source, game); + } } return true; } From f04786ee53e825841729f62a8463adfc7b68f14e Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:21:53 -0600 Subject: [PATCH 56/69] [DFT] Implement Coalstoke Gearhulk (#13385) --- .../src/mage/cards/c/CoalstokeGearhulk.java | 116 ++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 4 + 2 files changed, 120 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CoalstokeGearhulk.java diff --git a/Mage.Sets/src/mage/cards/c/CoalstokeGearhulk.java b/Mage.Sets/src/mage/cards/c/CoalstokeGearhulk.java new file mode 100644 index 00000000000..8e04f69764a --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CoalstokeGearhulk.java @@ -0,0 +1,116 @@ +package mage.cards.c; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.DelayedTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ExileTargetEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldWithCounterTargetEffect; +import mage.abilities.effects.common.SacrificeTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.Card; +import mage.constants.ComparisonType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.abilities.keyword.MenaceAbility; +import mage.abilities.keyword.DeathtouchAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.TargetController; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.Filter; +import mage.filter.StaticFilters; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.card.ManaValueLessThanControlledLandCountPredicate; +import mage.filter.predicate.mageobject.ManaValuePredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCardInGraveyard; +import mage.target.targetpointer.FixedTarget; + +/** + * + * @author anonymous + */ +public final class CoalstokeGearhulk extends CardImpl { + private static final FilterCreatureCard filter = new FilterCreatureCard("creature card with mana value 4 or less from a graveyard"); + + static { + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, 5)); + } + public CoalstokeGearhulk(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{1}{B}{B}{R}{R}"); + + this.subtype.add(SubType.CONSTRUCT); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // Menace + this.addAbility(new MenaceAbility()); + + // Deathtouch + this.addAbility(DeathtouchAbility.getInstance()); + + // When this creature enters, put target creature card with mana value 4 or less from a graveyard onto the battlefield under your control with a finality counter on it. That creature gains menace, deathtouch, and haste. At the beginnning of your next end step, exile that creature. + Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnFromGraveyardToBattlefieldWithCounterTargetEffect(CounterType.FINALITY.createInstance())) + .setTriggerPhrase("When this creature enters, "); + ability.addEffect(new GainAbilityTargetEffect(new MenaceAbility()) + .setText("That creature gains menace")); + ability.addEffect(new GainAbilityTargetEffect(DeathtouchAbility.getInstance()) + .setText("deathtouch") + .concatBy(",")); + ability.addEffect(new GainAbilityTargetEffect(HasteAbility.getInstance()) + .setText("haste") + .concatBy(", and")); + ability.addEffect(new CoalstokeGearhulkEffect()); + ability.addTarget(new TargetCardInGraveyard(filter)); + this.addAbility(ability); + } + + private CoalstokeGearhulk(final CoalstokeGearhulk card) { + super(card); + } + + @Override + public CoalstokeGearhulk copy() { + return new CoalstokeGearhulk(this); + } +} + +class CoalstokeGearhulkEffect extends OneShotEffect { + public CoalstokeGearhulkEffect() { + super(Outcome.Benefit); + this.staticText = "At the beginning of your next end step, exile that creature."; + } + + public CoalstokeGearhulkEffect(final CoalstokeGearhulkEffect effect) { + super(effect); + } + + @Override + public CoalstokeGearhulkEffect copy() { + return new CoalstokeGearhulkEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (permanent == null) { + return false; + } + ExileTargetEffect exileEffect = new ExileTargetEffect("At the beginning of your next end step, exile " + permanent.getLogName()); + exileEffect.setTargetPointer(new FixedTarget(permanent, game)); + DelayedTriggeredAbility delayedAbility = new AtTheBeginOfNextEndStepDelayedTriggeredAbility(exileEffect, TargetController.YOU); + game.addDelayedTriggeredAbility(delayedAbility, source); + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index dfcef6ecbec..16d9f565e22 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -68,6 +68,10 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Clamorous Ironclad", 117, Rarity.COMMON, mage.cards.c.ClamorousIronclad.class)); cards.add(new SetCardInfo("Cloudspire Captain", 9, Rarity.UNCOMMON, mage.cards.c.CloudspireCaptain.class)); cards.add(new SetCardInfo("Cloudspire Coordinator", 196, Rarity.UNCOMMON, mage.cards.c.CloudspireCoordinator.class)); + cards.add(new SetCardInfo("Coalstoke Gearhulk", 198, Rarity.MYTHIC, mage.cards.c.CoalstokeGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Coalstoke Gearhulk", 349, Rarity.MYTHIC, mage.cards.c.CoalstokeGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Coalstoke Gearhulk", 477, Rarity.MYTHIC, mage.cards.c.CoalstokeGearhulk.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Coalstoke Gearhulk", 548, Rarity.MYTHIC, mage.cards.c.CoalstokeGearhulk.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Collision Course", 10, Rarity.COMMON, mage.cards.c.CollisionCourse.class)); cards.add(new SetCardInfo("Count on Luck", 118, Rarity.RARE, mage.cards.c.CountOnLuck.class)); cards.add(new SetCardInfo("Country Roads", 253, Rarity.UNCOMMON, mage.cards.c.CountryRoads.class)); From 666397481296e7b4467914213ead8506a67a04af Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:42:00 -0600 Subject: [PATCH 57/69] [DFT] Implement Alacrian Armory (#13383) --- .../src/mage/cards/a/AlacrianArmory.java | 118 ++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 1 + 2 files changed, 119 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/a/AlacrianArmory.java diff --git a/Mage.Sets/src/mage/cards/a/AlacrianArmory.java b/Mage.Sets/src/mage/cards/a/AlacrianArmory.java new file mode 100644 index 00000000000..7f0f5f5286c --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AlacrianArmory.java @@ -0,0 +1,118 @@ +package mage.cards.a; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.CrewSaddleIncreasedPowerAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.EndTurnEffect; +import mage.abilities.effects.common.continuous.AddCardTypeTargetEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.keyword.CrewAbility; +import mage.abilities.keyword.SaddleAbility; +import mage.abilities.keyword.VigilanceAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.TargetController; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.permanent.SaddledSourceThisTurnPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; +import mage.target.targetpointer.FixedTarget; + +/** + * + * @author Jmlundeen + */ +public final class AlacrianArmory extends CardImpl { + + private static final FilterCreaturePermanent creatureFilter = new FilterCreaturePermanent("Creatures you control"); + private static final FilterPermanent mountOrVehicleFilter = new FilterPermanent("Mount or Vehicle you control"); + + static { + mountOrVehicleFilter.add(Predicates.or( + SubType.MOUNT.getPredicate(), + SubType.VEHICLE.getPredicate() + )); + mountOrVehicleFilter.add(TargetController.YOU.getControllerPredicate()); + } + public AlacrianArmory(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{3}{W}"); + + // Creatures you control get +0/+1 and have vigilance. + Ability staticAbility = new SimpleStaticAbility(new BoostControlledEffect(0, 1, Duration.WhileOnBattlefield, creatureFilter)); + Effect effect = new GainAbilityControlledEffect(VigilanceAbility.getInstance(), Duration.WhileOnBattlefield, creatureFilter); + effect.setText("and have vigilance"); + staticAbility.addEffect(effect); + this.addAbility(staticAbility); + // At the beginning of combat on your turn, choose up to one target Mount or Vehicle you control. Until end of turn, that permanent becomes saddled if it's a Mount and becomes an artifact creature if it's a Vehicle. + Ability animateAbility = new BeginningOfCombatTriggeredAbility( + new AlacrianArmoryAnimateEffect() + ); + animateAbility.addTarget(new TargetPermanent(0, 1, mountOrVehicleFilter)); + this.addAbility(animateAbility); + } + + private AlacrianArmory(final AlacrianArmory card) { + super(card); + } + + @Override + public AlacrianArmory copy() { + return new AlacrianArmory(this); + } +} + +class AlacrianArmoryAnimateEffect extends OneShotEffect { + + AlacrianArmoryAnimateEffect() { + super(Outcome.Benefit); + staticText = "choose up to one target Mount or Vehicle you control. " + + "Until end of turn, that permanent becomes saddled if it's a Mount " + + "and becomes an artifact creature if it's a Vehicle"; + } + + private AlacrianArmoryAnimateEffect(final AlacrianArmoryAnimateEffect effect) { + super(effect); + } + + @Override + public AlacrianArmoryAnimateEffect copy() { + return new AlacrianArmoryAnimateEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent target = game.getPermanent(source.getFirstTarget()); + if (target == null) { + return false; + } + if (target.hasSubtype(SubType.MOUNT, game)) { + target.getAbilities().stream().filter( + ability -> ability instanceof SaddleAbility) + .findFirst() + .ifPresent(ability -> game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.MOUNT_SADDLED, + ability.getSourceId(), + ability, source.getControllerId())) + ); + } + if (target.hasSubtype(SubType.VEHICLE, game)) { + game.addEffect(new AddCardTypeTargetEffect(Duration.EndOfTurn, CardType.CREATURE, CardType.ARTIFACT) + .setTargetPointer(new FixedTarget(target, game)), source); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 16d9f565e22..3b72036604f 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -28,6 +28,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Afterburner Expert", 150, Rarity.RARE, mage.cards.a.AfterburnerExpert.class)); cards.add(new SetCardInfo("Agonasaur Rex", 151, Rarity.RARE, mage.cards.a.AgonasaurRex.class)); cards.add(new SetCardInfo("Air Response Unit", 1, Rarity.UNCOMMON, mage.cards.a.AirResponseUnit.class)); + cards.add(new SetCardInfo("Alacrian Armory", 2, Rarity.UNCOMMON, mage.cards.a.AlacrianArmory.class)); cards.add(new SetCardInfo("Alacrian Jaguar", 152, Rarity.COMMON, mage.cards.a.AlacrianJaguar.class)); cards.add(new SetCardInfo("Amonkhet Raceway", 248, Rarity.UNCOMMON, mage.cards.a.AmonkhetRaceway.class)); cards.add(new SetCardInfo("Ancient Vendetta", 75, Rarity.UNCOMMON, mage.cards.a.AncientVendetta.class)); From 532490553dcf2bccd1a6f6340aef671bf214c6bd Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Tue, 25 Feb 2025 17:24:58 -0600 Subject: [PATCH 58/69] [DFT] Implement Cursecloth Wrappings (#13386) --- .../src/mage/cards/c/CurseclothWrappings.java | 90 +++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 5 ++ 2 files changed, 95 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CurseclothWrappings.java diff --git a/Mage.Sets/src/mage/cards/c/CurseclothWrappings.java b/Mage.Sets/src/mage/cards/c/CurseclothWrappings.java new file mode 100644 index 00000000000..a74c3ed7b36 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CurseclothWrappings.java @@ -0,0 +1,90 @@ +package mage.cards.c; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.keyword.EmbalmAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; +import mage.constants.SubType; +import mage.filter.common.FilterCreatureCard; +import mage.filter.common.FilterCreaturePermanent; +import mage.game.Game; +import mage.target.common.TargetCardInYourGraveyard; + +/** + * + * @author Jmlundeen + */ +public final class CurseclothWrappings extends CardImpl { + public static final FilterCreaturePermanent filter = new FilterCreaturePermanent("Zombies you control"); + + static { + filter.add(SubType.ZOMBIE.getPredicate()); + } + public CurseclothWrappings(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{B}{B}"); + + + // Zombies you control get +1/+1. + this.addAbility(new SimpleStaticAbility(new BoostControlledEffect(1, 1, Duration.WhileOnBattlefield, filter))); + // {T}: Target creature card in your graveyard gains embalm until end of turn. The embalm cost is equal to its mana cost. + Ability ability = new SimpleActivatedAbility( + new CurseClothWrappingsEffect(), + new TapSourceCost() + ); + ability.addTarget(new TargetCardInYourGraveyard(new FilterCreatureCard("creature card in your graveyard"))); + this.addAbility(ability); + } + + private CurseclothWrappings(final CurseclothWrappings card) { + super(card); + } + + @Override + public CurseclothWrappings copy() { + return new CurseclothWrappings(this); + } +} + +class CurseClothWrappingsEffect extends ContinuousEffectImpl { + public CurseClothWrappingsEffect() { + super(Duration.EndOfTurn, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + this.staticText = "Target creature card in your graveyard gains embalm until end of turn. " + + "The embalm cost is equal to its mana cost. (Exile that card and pay its embalm cost: " + + "Create a token that's a copy of it, except it's a white Zombie in addition to its other types " + + "and has no mana cost. Embalm only as a sorcery.)"; + } + + public CurseClothWrappingsEffect(final CurseClothWrappingsEffect effect) { + super(effect); + } + + @Override + public CurseClothWrappingsEffect copy() { + return new CurseClothWrappingsEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Card card = game.getCard(getTargetPointer().getFirst(game, source)); + if (card != null) { + EmbalmAbility embalmAbility = new EmbalmAbility(card.getManaCost(), card); + game.getState().addOtherAbility(card, embalmAbility); + return true; + } + return false; + } + +} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 3b72036604f..90384a4214c 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -78,6 +78,11 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Country Roads", 253, Rarity.UNCOMMON, mage.cards.c.CountryRoads.class)); cards.add(new SetCardInfo("Crash and Burn", 119, Rarity.COMMON, mage.cards.c.CrashAndBurn.class)); cards.add(new SetCardInfo("Cryptcaller Chariot", 80, Rarity.RARE, mage.cards.c.CryptcallerChariot.class)); + cards.add(new SetCardInfo("Cursecloth Wrappings", 81, Rarity.RARE, mage.cards.c.CurseclothWrappings.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Cursecloth Wrappings", 383, Rarity.RARE, mage.cards.c.CurseclothWrappings.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Cursecloth Wrappings", 400, Rarity.MYTHIC, mage.cards.c.CurseclothWrappings.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Cursecloth Wrappings", 410, Rarity.MYTHIC, mage.cards.c.CurseclothWrappings.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Cursecloth Wrappings", 447, Rarity.RARE, mage.cards.c.CurseclothWrappings.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Daretti, Rocketeer Engineer", 120, Rarity.RARE, mage.cards.d.DarettiRocketeerEngineer.class)); cards.add(new SetCardInfo("Daring Mechanic", 11, Rarity.COMMON, mage.cards.d.DaringMechanic.class)); cards.add(new SetCardInfo("Deathless Pilot", 82, Rarity.COMMON, mage.cards.d.DeathlessPilot.class)); From a20e304234d00f13788ec551c534b2f10c366f1a Mon Sep 17 00:00:00 2001 From: Asa Reynolds <93608673+asareynolds@users.noreply.github.com> Date: Wed, 26 Feb 2025 01:39:40 -0500 Subject: [PATCH 59/69] gui: fixed typo in mana pool dialog (#13391) --- .../Mage.Player.Human/src/mage/player/human/HumanPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index f6618532f77..f35221b49eb 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -2951,7 +2951,7 @@ public class HumanPlayer extends PlayerImpl { protected boolean passWithManaPoolCheck(Game game) { if (userData.confirmEmptyManaPool() && game.getStack().isEmpty() && getManaPool().count() > 0 && getManaPool().canLostManaOnEmpty()) { - String message = GameLog.getPlayerConfirmColoredText("You still have mana in your mana pool and it will be lose. Pass anyway?"); + String message = GameLog.getPlayerConfirmColoredText("You still have mana in your mana pool and it will be lost. Pass anyway?"); if (!chooseUse(Outcome.Detriment, message, null, game)) { sendPlayerAction(PlayerAction.PASS_PRIORITY_CANCEL_ALL_ACTIONS, game, null); return false; From 705ce2320d3a28fe961755d585a4d5be5f0233b2 Mon Sep 17 00:00:00 2001 From: ssk97 Date: Wed, 26 Feb 2025 00:14:48 -0800 Subject: [PATCH 60/69] [DFT] Canyon Vaulter, Cloudspire Skycycle, Dune Drifter (#13382) * Support Mounts and Your Main restriction in CrewsVehicleSourceTriggeredAbility * Genericize Reckless Velocitaur * Implement [DFT] Canyon Vaulter, Cloudspire Skycycle, and Dune Drifter --- Mage.Sets/src/mage/cards/c/CanyonVaulter.java | 43 +++++++++++++ .../src/mage/cards/c/CloudspireSkycycle.java | 60 +++++++++++++++++++ Mage.Sets/src/mage/cards/d/DuneDrifter.java | 57 ++++++++++++++++++ .../src/mage/cards/r/RecklessVelocitaur.java | 58 ++++-------------- Mage.Sets/src/mage/sets/Aetherdrift.java | 3 + .../CrewsVehicleSourceTriggeredAbility.java | 19 +++++- 6 files changed, 192 insertions(+), 48 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CanyonVaulter.java create mode 100644 Mage.Sets/src/mage/cards/c/CloudspireSkycycle.java create mode 100644 Mage.Sets/src/mage/cards/d/DuneDrifter.java diff --git a/Mage.Sets/src/mage/cards/c/CanyonVaulter.java b/Mage.Sets/src/mage/cards/c/CanyonVaulter.java new file mode 100644 index 00000000000..7141b745670 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CanyonVaulter.java @@ -0,0 +1,43 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.CrewsVehicleSourceTriggeredAbility; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author notgreat + */ +public final class CanyonVaulter extends CardImpl { + + public CanyonVaulter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W}"); + + this.subtype.add(SubType.KOR); + this.subtype.add(SubType.PILOT); + this.power = new MageInt(3); + this.toughness = new MageInt(1); + + // Whenever this creature saddles a Mount or crews a Vehicle during your main phase, that Mount or Vehicle gains flying until end of turn. + Effect effect = new GainAbilityTargetEffect(FlyingAbility.getInstance(), Duration.EndOfTurn); + effect.setText("that Mount or Vehicle gains flying until end of turn"); + this.addAbility(new CrewsVehicleSourceTriggeredAbility(effect, true, true)); + } + + private CanyonVaulter(final CanyonVaulter card) { + super(card); + } + + @Override + public CanyonVaulter copy() { + return new CanyonVaulter(this); + } +} diff --git a/Mage.Sets/src/mage/cards/c/CloudspireSkycycle.java b/Mage.Sets/src/mage/cards/c/CloudspireSkycycle.java new file mode 100644 index 00000000000..fe62143da4b --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CloudspireSkycycle.java @@ -0,0 +1,60 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.counter.DistributeCountersEffect; +import mage.abilities.keyword.CrewAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.target.common.TargetPermanentAmount; + +import java.util.UUID; + +/** + * @author notgreat + */ +public final class CloudspireSkycycle extends CardImpl { + + private static final FilterControlledPermanent filter = new FilterControlledPermanent("other target Vehicles and/or creatures you control"); + + static { + filter.add(AnotherPredicate.instance); + filter.add(Predicates.or(SubType.VEHICLE.getPredicate(), CardType.CREATURE.getPredicate())); + } + + public CloudspireSkycycle(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{2}{R}{W}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // When this Vehicle enters, distribute two +1/+1 counters among one or two other target Vehicles and/or creatures you control. + Ability ability = new EntersBattlefieldTriggeredAbility(new DistributeCountersEffect()); + ability.addTarget(new TargetPermanentAmount(2, 1, filter)); + this.addAbility(ability); + + // Crew 1 + this.addAbility(new CrewAbility(1)); + + } + + private CloudspireSkycycle(final CloudspireSkycycle card) { + super(card); + } + + @Override + public CloudspireSkycycle copy() { + return new CloudspireSkycycle(this); + } +} diff --git a/Mage.Sets/src/mage/cards/d/DuneDrifter.java b/Mage.Sets/src/mage/cards/d/DuneDrifter.java new file mode 100644 index 00000000000..8a3f5a6307b --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DuneDrifter.java @@ -0,0 +1,57 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.keyword.CrewAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.ComparisonType; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.targetadjustment.XManaValueTargetAdjuster; + +import java.util.UUID; + +/** + * @author notgreat + */ +public final class DuneDrifter extends CardImpl { + + private static final FilterCard filter = new FilterCard("artifact or creature card with mana value X or less from your graveyard"); + + static { + filter.add(Predicates.or(CardType.ARTIFACT.getPredicate(), CardType.CREATURE.getPredicate())); + } + + public DuneDrifter(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{X}{W}{B}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // When this Vehicle enters, return target artifact or creature card with mana value X or less from your graveyard to the battlefield. + Ability ability = new EntersBattlefieldTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect()); + ability.addTarget(new TargetCardInYourGraveyard(filter)); + ability.setTargetAdjuster(new XManaValueTargetAdjuster(ComparisonType.OR_LESS)); + this.addAbility(ability); + + // Crew 2 + this.addAbility(new CrewAbility(2)); + + } + + private DuneDrifter(final DuneDrifter card) { + super(card); + } + + @Override + public DuneDrifter copy() { + return new DuneDrifter(this); + } +} diff --git a/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java b/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java index 66781d9082d..a6fd111e961 100644 --- a/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java +++ b/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java @@ -1,18 +1,17 @@ package mage.cards.r; import mage.MageInt; -import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.Ability; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.CrewsVehicleSourceTriggeredAbility; import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; import mage.abilities.keyword.TrampleAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; +import mage.constants.Duration; import mage.constants.SubType; -import mage.constants.Zone; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.target.targetpointer.FixedTarget; import java.util.UUID; @@ -30,7 +29,14 @@ public final class RecklessVelocitaur extends CardImpl { this.toughness = new MageInt(3); // Whenever this creature saddles a Mount or crews a Vehicle during your main phase, that Mount or Vehicle gets +2/+0 and gains trample until end of turn. - this.addAbility(new RecklessVelocitaurTriggeredAbility()); + + Effect boostEffect = new BoostTargetEffect(1, 1, Duration.EndOfTurn) + .setText("that Mount or Vehicle gets +2/+0"); + Effect abilityGainEffect = new GainAbilityTargetEffect(TrampleAbility.getInstance(), Duration.EndOfTurn) + .setText("and gains trample until end of turn"); + Ability ability = new CrewsVehicleSourceTriggeredAbility(boostEffect, true, true); + ability.addEffect(abilityGainEffect); + this.addAbility(ability); } private RecklessVelocitaur(final RecklessVelocitaur card) { @@ -42,43 +48,3 @@ public final class RecklessVelocitaur extends CardImpl { return new RecklessVelocitaur(this); } } - -class RecklessVelocitaurTriggeredAbility extends TriggeredAbilityImpl { - - RecklessVelocitaurTriggeredAbility() { - super(Zone.BATTLEFIELD, new BoostTargetEffect(2, 0).setText("that Mount or Vehicle gets +2/+0")); - this.addEffect(new GainAbilityTargetEffect(TrampleAbility.getInstance()).setText("and gains trample until end of turn")); - this.setTriggerPhrase("Whenever {this} saddles a Mount or crews a Vehicle during your main phase, "); - } - - private RecklessVelocitaurTriggeredAbility(final RecklessVelocitaurTriggeredAbility ability) { - super(ability); - } - - @Override - public RecklessVelocitaurTriggeredAbility copy() { - return new RecklessVelocitaurTriggeredAbility(this); - } - - @Override - public boolean checkEventType(GameEvent event, Game game) { - switch (event.getType()) { - case SADDLED_MOUNT: - case CREWED_VEHICLE: - return true; - default: - return false; - } - } - - @Override - public boolean checkTrigger(GameEvent event, Game game) { - if (!this.isControlledBy(game.getActivePlayerId()) - || !game.isMainPhase() - || !event.getTargetId().equals(this.getSourceId())) { - return false; - } - this.getEffects().setTargetPointer(new FixedTarget(event.getSourceId())); - return true; - } -} diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 90384a4214c..7e036201459 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -62,6 +62,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Burnout Bashtronaut", 115, Rarity.RARE, mage.cards.b.BurnoutBashtronaut.class)); cards.add(new SetCardInfo("Caelorna, Coral Tyrant", 40, Rarity.UNCOMMON, mage.cards.c.CaelornaCoralTyrant.class)); cards.add(new SetCardInfo("Camera Launcher", 232, Rarity.COMMON, mage.cards.c.CameraLauncher.class)); + cards.add(new SetCardInfo("Canyon Vaulter", 8, Rarity.UNCOMMON, mage.cards.c.CanyonVaulter.class)); cards.add(new SetCardInfo("Caradora, Heart of Alacria", 195, Rarity.RARE, mage.cards.c.CaradoraHeartOfAlacria.class)); cards.add(new SetCardInfo("Carrion Cruiser", 78, Rarity.UNCOMMON, mage.cards.c.CarrionCruiser.class)); cards.add(new SetCardInfo("Chandra, Spark Hunter", 116, Rarity.MYTHIC, mage.cards.c.ChandraSparkHunter.class)); @@ -69,6 +70,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Clamorous Ironclad", 117, Rarity.COMMON, mage.cards.c.ClamorousIronclad.class)); cards.add(new SetCardInfo("Cloudspire Captain", 9, Rarity.UNCOMMON, mage.cards.c.CloudspireCaptain.class)); cards.add(new SetCardInfo("Cloudspire Coordinator", 196, Rarity.UNCOMMON, mage.cards.c.CloudspireCoordinator.class)); + cards.add(new SetCardInfo("Cloudspire Skycycle", 197, Rarity.UNCOMMON, mage.cards.c.CloudspireSkycycle.class)); cards.add(new SetCardInfo("Coalstoke Gearhulk", 198, Rarity.MYTHIC, mage.cards.c.CoalstokeGearhulk.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Coalstoke Gearhulk", 349, Rarity.MYTHIC, mage.cards.c.CoalstokeGearhulk.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Coalstoke Gearhulk", 477, Rarity.MYTHIC, mage.cards.c.CoalstokeGearhulk.class, NON_FULL_USE_VARIOUS)); @@ -95,6 +97,7 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Draconautics Engineer", 121, Rarity.RARE, mage.cards.d.DraconauticsEngineer.class)); cards.add(new SetCardInfo("Dracosaur Auxiliary", 122, Rarity.UNCOMMON, mage.cards.d.DracosaurAuxiliary.class)); cards.add(new SetCardInfo("Dredger's Insight", 159, Rarity.UNCOMMON, mage.cards.d.DredgersInsight.class)); + 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("Embalmed Ascendant", 201, Rarity.UNCOMMON, mage.cards.e.EmbalmedAscendant.class)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/CrewsVehicleSourceTriggeredAbility.java b/Mage/src/main/java/mage/abilities/effects/common/CrewsVehicleSourceTriggeredAbility.java index 3f684fa2ff3..cac0128b939 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/CrewsVehicleSourceTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/effects/common/CrewsVehicleSourceTriggeredAbility.java @@ -10,14 +10,26 @@ import mage.target.targetpointer.FixedTarget; public class CrewsVehicleSourceTriggeredAbility extends TriggeredAbilityImpl { + private final boolean mountsAlso; + private final boolean yourMainPhaseOnly; + public CrewsVehicleSourceTriggeredAbility(Effect effect) { + this(effect, false, false); + } + + public CrewsVehicleSourceTriggeredAbility(Effect effect, boolean mountsAlso, boolean yourMainPhaseOnly) { super(Zone.BATTLEFIELD, effect, false); this.addIcon(CardIconImpl.ABILITY_CREW); - setTriggerPhrase("Whenever {this} crews a Vehicle, "); + this.mountsAlso = mountsAlso; + this.yourMainPhaseOnly = yourMainPhaseOnly; + setTriggerPhrase("Whenever {this}" + (mountsAlso ? " saddles a Mount or" : "") + + " crews a Vehicle" + (yourMainPhaseOnly ? " during your main phase" : "") + ", "); } protected CrewsVehicleSourceTriggeredAbility(final CrewsVehicleSourceTriggeredAbility ability) { super(ability); + this.mountsAlso = ability.mountsAlso; + this.yourMainPhaseOnly = ability.yourMainPhaseOnly; } @Override @@ -27,11 +39,14 @@ public class CrewsVehicleSourceTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkEventType(GameEvent event, Game game) { - return event.getType() == GameEvent.EventType.CREWED_VEHICLE; + return event.getType() == GameEvent.EventType.CREWED_VEHICLE || (mountsAlso && event.getType() == GameEvent.EventType.SADDLED_MOUNT); } @Override public boolean checkTrigger(GameEvent event, Game game) { + if (yourMainPhaseOnly && !(game.isMainPhase() && this.isControlledBy(game.getActivePlayerId()))) { + return false; + } if (event.getTargetId().equals(getSourceId())) { for (Effect effect : getEffects()) { // set the vehicle id as target From e5e3934b20aa0dd44c9725c29290a97223200481 Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Wed, 26 Feb 2025 05:41:41 -0600 Subject: [PATCH 61/69] [DFT] Implement Demonic Junker (#13387) --- Mage.Sets/src/mage/cards/d/DemonicJunker.java | 117 ++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 4 + Utils/keywords.txt | 1 + 3 files changed, 122 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DemonicJunker.java diff --git a/Mage.Sets/src/mage/cards/d/DemonicJunker.java b/Mage.Sets/src/mage/cards/d/DemonicJunker.java new file mode 100644 index 00000000000..85d6415c796 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DemonicJunker.java @@ -0,0 +1,117 @@ +package mage.cards.d; + +import java.util.*; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.abilities.keyword.AffinityForArtifactsAbility; +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.FilterCreaturePermanent; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.targetadjustment.TargetAdjuster; +import mage.target.targetpointer.EachTargetPointer; + +/** + * + * @author Jmlundeen + */ +public final class DemonicJunker extends CardImpl { + + public DemonicJunker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT}, "{6}{B}"); + + this.subtype.add(SubType.VEHICLE); + this.power = new MageInt(4); + this.toughness = new MageInt(3); + + // Affinity for artifacts + this.addAbility(new AffinityForArtifactsAbility()); + + // When this Vehicle enters, for each player, destroy up to one target creature that player controls. If a creature you controlled was destroyed this way, put two +1/+1 counters on this Vehicle. + Ability ability = new EntersBattlefieldTriggeredAbility(new DemonicJunkerEffect() + .setTargetPointer(new EachTargetPointer())) + .setTriggerPhrase("When this Vehicle enters, "); + this.addAbility(ability.setTargetAdjuster(DemonicJunkerAdjuster.instance)); + + // Crew 2 + this.addAbility(new CrewAbility(2)); + + } + + private DemonicJunker(final DemonicJunker card) { + super(card); + } + + @Override + public DemonicJunker copy() { + return new DemonicJunker(this); + } +} + +enum DemonicJunkerAdjuster implements TargetAdjuster { + instance; + @Override + public void adjustTargets(Ability ability, Game game) { + ability.getTargets().clear(); + for (UUID playerId : game.getState().getPlayersInRange(ability.getControllerId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + String playerName = ability.isControlledBy(playerId) ? "you" : player.getName(); + FilterCreaturePermanent filter = new FilterCreaturePermanent("creature controlled by " + playerName); + filter.add(new ControllerIdPredicate(playerId)); + ability.addTarget(new TargetPermanent(0, 1, filter)); + } + } +} + +class DemonicJunkerEffect extends OneShotEffect { + + DemonicJunkerEffect() { + super(Outcome.DestroyPermanent); + staticText = "for each player, destroy up to one target creature that player controls. If a creature you controlled was destroyed this way, put two +1/+1 counters on this Vehicle"; + } + + public DemonicJunkerEffect(final DemonicJunkerEffect effect) { + super(effect); + } + + @Override + public DemonicJunkerEffect copy() { + return new DemonicJunkerEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + boolean giveCounters = false; + for (UUID permanentId : getTargetPointer().getTargets(game, source)) { + Permanent permanent = game.getPermanent(permanentId); + if (permanent == null) { + continue; + } + if (permanent.destroy(source, game, false)) { + giveCounters = permanent.getControllerId().equals(source.getControllerId()); + } + if (giveCounters) { + Permanent vehicle = game.getPermanent(source.getSourceId()); + if (vehicle != null) { + vehicle.addCounters(CounterType.P1P1.createInstance(2), source.getControllerId(), source, game); + } + } + } + return true; + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 7e036201459..7f744a7ef30 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -90,6 +90,10 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Deathless Pilot", 82, Rarity.COMMON, mage.cards.d.DeathlessPilot.class)); cards.add(new SetCardInfo("Debris Beetle", 199, Rarity.RARE, mage.cards.d.DebrisBeetle.class)); cards.add(new SetCardInfo("Defend the Rider", 157, Rarity.UNCOMMON, mage.cards.d.DefendTheRider.class)); + cards.add(new SetCardInfo("Demonic Junker", 83, Rarity.RARE, mage.cards.d.DemonicJunker.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Demonic Junker", 307, Rarity.RARE, mage.cards.d.DemonicJunker.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Demonic Junker", 448, Rarity.RARE, mage.cards.d.DemonicJunker.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Demonic Junker", 524, Rarity.RARE, mage.cards.d.DemonicJunker.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Detention Chariot", 12, Rarity.UNCOMMON, mage.cards.d.DetentionChariot.class)); cards.add(new SetCardInfo("Dismal Backwater", 254, Rarity.COMMON, mage.cards.d.DismalBackwater.class)); cards.add(new SetCardInfo("District Mascot", 158, Rarity.RARE, mage.cards.d.DistrictMascot.class)); diff --git a/Utils/keywords.txt b/Utils/keywords.txt index 810e9e1c142..f4b661ac0e6 100644 --- a/Utils/keywords.txt +++ b/Utils/keywords.txt @@ -1,3 +1,4 @@ +Affinity for artifacts|new| Afflict|number| Afterlife|number| Annihilator|number| From 81aea698157542aa5c736df2ff52b0750f629b7a Mon Sep 17 00:00:00 2001 From: Jmlundeen <98545818+Jmlundeen@users.noreply.github.com> Date: Wed, 26 Feb 2025 05:44:59 -0600 Subject: [PATCH 62/69] [DFT] Implement Captain Howler, Sea Scourge (#13384) * [DFT] Implement Captain Howler, Sea Scourge * Fix styling --------- Co-authored-by: Oleg Agafonov --- .../mage/cards/c/CaptainHowlerSeaScourge.java | 69 +++++++++++++++++++ Mage.Sets/src/mage/sets/Aetherdrift.java | 3 + 2 files changed, 72 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/c/CaptainHowlerSeaScourge.java diff --git a/Mage.Sets/src/mage/cards/c/CaptainHowlerSeaScourge.java b/Mage.Sets/src/mage/cards/c/CaptainHowlerSeaScourge.java new file mode 100644 index 00000000000..cca3f877e6d --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CaptainHowlerSeaScourge.java @@ -0,0 +1,69 @@ +package mage.cards.c; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.costs.CompositeCost; +import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.MultipliedValue; +import mage.abilities.dynamicvalue.common.SavedDiscardValue; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.common.DiscardOneOrMoreCardsTriggeredAbility; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +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.target.common.TargetCreaturePermanent; + +/** + * + * @author Jmlundeen + */ +public final class CaptainHowlerSeaScourge extends CardImpl { + private static final DynamicValue powerValue = new MultipliedValue(SavedDiscardValue.MANY, 2); + + public CaptainHowlerSeaScourge(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{U}{R}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.SHARK); + this.subtype.add(SubType.PIRATE); + this.power = new MageInt(5); + this.toughness = new MageInt(4); + + // Ward--{2}, Pay 2 life. + CompositeCost cost = new CompositeCost(new ManaCostsImpl<>("{2}"), new PayLifeCost(2), "{2}, Pay 2 life"); + this.addAbility(new WardAbility(cost, false)); + + // Whenever you discard one or more cards, target creature gets +2/+0 until end of turn for each card discarded this way. Whenever that creature deals combat damage to a player this turn, you draw a card. + Ability ability = new DiscardOneOrMoreCardsTriggeredAbility( + new BoostTargetEffect(powerValue, StaticValue.get(0)) + .setText("target creature gets +2/+0 until end of turn for each card discarded this way") + ); + ability.addEffect(new GainAbilityTargetEffect( + new DealsCombatDamageToAPlayerTriggeredAbility( + new DrawCardSourceControllerEffect(1), false + )) + .setText("Whenever that creature deals combat damage to a player this turn, you draw a card") + ); + ability.addTarget(new TargetCreaturePermanent()); + this.addAbility(ability); + } + + private CaptainHowlerSeaScourge(final CaptainHowlerSeaScourge card) { + super(card); + } + + @Override + public CaptainHowlerSeaScourge copy() { + return new CaptainHowlerSeaScourge(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/Aetherdrift.java b/Mage.Sets/src/mage/sets/Aetherdrift.java index 7f744a7ef30..fa0196632dd 100644 --- a/Mage.Sets/src/mage/sets/Aetherdrift.java +++ b/Mage.Sets/src/mage/sets/Aetherdrift.java @@ -62,6 +62,9 @@ public final class Aetherdrift extends ExpansionSet { cards.add(new SetCardInfo("Burnout Bashtronaut", 115, Rarity.RARE, mage.cards.b.BurnoutBashtronaut.class)); cards.add(new SetCardInfo("Caelorna, Coral Tyrant", 40, Rarity.UNCOMMON, mage.cards.c.CaelornaCoralTyrant.class)); cards.add(new SetCardInfo("Camera Launcher", 232, Rarity.COMMON, mage.cards.c.CameraLauncher.class)); + cards.add(new SetCardInfo("Captain Howler, Sea Scourge", 194, Rarity.RARE, mage.cards.c.CaptainHowlerSeaScourge.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Captain Howler, Sea Scourge", 361, Rarity.RARE, mage.cards.c.CaptainHowlerSeaScourge.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Captain Howler, Sea Scourge", 475, Rarity.RARE, mage.cards.c.CaptainHowlerSeaScourge.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Canyon Vaulter", 8, Rarity.UNCOMMON, mage.cards.c.CanyonVaulter.class)); cards.add(new SetCardInfo("Caradora, Heart of Alacria", 195, Rarity.RARE, mage.cards.c.CaradoraHeartOfAlacria.class)); cards.add(new SetCardInfo("Carrion Cruiser", 78, Rarity.UNCOMMON, mage.cards.c.CarrionCruiser.class)); From f83a162d304292ae9dd82f35b1ad6381eb7b934a Mon Sep 17 00:00:00 2001 From: PurpleCrowbar <26198472+PurpleCrowbar@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:19:07 +0000 Subject: [PATCH 63/69] Achieve parity with 411100e, improve hints button text --- .../src/main/java/mage/client/game/PlayerPanelExt.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java index 6d7ac145d5e..3ba80d606d9 100644 --- a/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java +++ b/Mage.Client/src/main/java/mage/client/game/PlayerPanelExt.java @@ -687,11 +687,11 @@ public class PlayerPanelExt extends javax.swing.JPanel { zonesPanel.setLayout(null); zonesPanel.setOpaque(false); - // tools button like hints + // hints toolHintsHelper = new JButton(); toolHintsHelper.setFont(this.getFont()); - toolHintsHelper.setText("hints"); - toolHintsHelper.setToolTipText("Open new card hints helper window"); + toolHintsHelper.setText("Hints"); + toolHintsHelper.setToolTipText("Open card hints helper window"); toolHintsHelper.addActionListener(this::btnToolHintsHelperActionPerformed); toolHintsHelper.setBounds(sizeMod(3), sizeMod(2 + 21 + 2), sizeMod(73), sizeMod(21)); zonesPanel.add(toolHintsHelper); @@ -701,7 +701,7 @@ public class PlayerPanelExt extends javax.swing.JPanel { image = ImageHelper.getImageFromResources("/info/command_zone.png"); resized = ImageHelper.getResizedImage(BufferedImageBuilder.bufferImage(image, BufferedImage.TYPE_INT_ARGB), r); commandZone = new HoverButton(null, resized, resized, resized, r, this.guiScaleMod); - commandZone.setToolTipText("Command Zone (Commanders, Emblems and Planes)"); + commandZone.setToolTipText("Command Zone (Commanders, Emblems, and Planes)"); commandZone.setOpaque(false); commandZone.setObserver(() -> btnCommandZoneActionPerformed(null)); commandZone.setBounds(sizeMod(3), 0, sizeMod(21), sizeMod(21)); From 8ed414af9671ec76639788b17e600cd02c90c57e Mon Sep 17 00:00:00 2001 From: Steven Knipe Date: Wed, 26 Feb 2025 18:03:26 -0800 Subject: [PATCH 64/69] Fix wrong values from my update to RecklessVelocitaur --- Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java b/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java index a6fd111e961..c2a00bb8b32 100644 --- a/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java +++ b/Mage.Sets/src/mage/cards/r/RecklessVelocitaur.java @@ -30,7 +30,7 @@ public final class RecklessVelocitaur extends CardImpl { // Whenever this creature saddles a Mount or crews a Vehicle during your main phase, that Mount or Vehicle gets +2/+0 and gains trample until end of turn. - Effect boostEffect = new BoostTargetEffect(1, 1, Duration.EndOfTurn) + Effect boostEffect = new BoostTargetEffect(2, 0, Duration.EndOfTurn) .setText("that Mount or Vehicle gets +2/+0"); Effect abilityGainEffect = new GainAbilityTargetEffect(TrampleAbility.getInstance(), Duration.EndOfTurn) .setText("and gains trample until end of turn"); From 419030b681f654a849f58d69ad8fc58cc77b3302 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Wed, 26 Feb 2025 23:12:07 -0500 Subject: [PATCH 65/69] [NCC] Partially fix Rain of Riches and add tests. Two tests are failing and ignored because this is only a partial fix, as we will still need to process actions between the last mana being paid and the spell being cast. --- Mage.Sets/src/mage/cards/r/RainOfRiches.java | 95 +++++--------- .../cards/single/ncc/RainOfRichesTest.java | 118 ++++++++++++++++++ .../GainAbilityControlledSpellsEffect.java | 12 +- 3 files changed, 155 insertions(+), 70 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java diff --git a/Mage.Sets/src/mage/cards/r/RainOfRiches.java b/Mage.Sets/src/mage/cards/r/RainOfRiches.java index b15dcdaee2f..c87c6ea05d3 100644 --- a/Mage.Sets/src/mage/cards/r/RainOfRiches.java +++ b/Mage.Sets/src/mage/cards/r/RainOfRiches.java @@ -1,22 +1,24 @@ package mage.cards.r; import mage.MageObjectReference; -import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; -import mage.abilities.condition.Condition; -import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledSpellsEffect; import mage.abilities.keyword.CascadeAbility; +import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; +import mage.filter.common.FilterNonlandCard; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; import mage.game.Game; import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; import mage.game.permanent.token.TreasureToken; import mage.game.stack.Spell; import mage.game.stack.StackObject; -import mage.players.Player; import mage.watchers.Watcher; import mage.watchers.common.ManaPaidSourceWatcher; @@ -25,10 +27,18 @@ import java.util.Map; import java.util.UUID; /** - * @author Alex-Vasile + * @author Alex-Vasile, Susucr */ public class RainOfRiches extends CardImpl { + + private static final FilterNonlandCard filter = + new FilterNonlandCard("The first spell you cast each turn that mana from a Treasure was spent to cast"); + + static { + filter.add(RainOfRichesPredicate.instance); + } + public RainOfRiches(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{R}{R}"); @@ -40,7 +50,7 @@ public class RainOfRiches extends CardImpl { // You may cast it without paying its mana cost. // Put the exiled cards on the bottom of your library in a random order.) this.addAbility( - new SimpleStaticAbility(new RainOfRichesGainsCascadeEffect()), + new SimpleStaticAbility(new GainAbilityControlledSpellsEffect(new CascadeAbility(false), filter)), new RainOfRichesWatcher() ); } @@ -55,65 +65,20 @@ public class RainOfRiches extends CardImpl { } } -class RainOfRichesGainsCascadeEffect extends ContinuousEffectImpl { - - private final Ability cascadeAbility = new CascadeAbility(); - - RainOfRichesGainsCascadeEffect() { - super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); - this.staticText = - "The first spell you cast each turn that mana from a Treasure was spent to cast has cascade. " + - "(When you cast the spell, exile cards from the top of your library until you exile a nonland card that costs less. " + - "You may cast it without paying its mana cost. " + - "Put the exiled cards on the bottom of your library in a random order.)"; - } - - private RainOfRichesGainsCascadeEffect(final RainOfRichesGainsCascadeEffect effect) { - super(effect); - } - - @Override - public boolean apply(Game game, Ability source) { - Player controller = game.getPlayer(source.getControllerId()); - RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class); - if (controller == null || watcher == null) { - return false; - } - - for (StackObject stackObject : game.getStack()) { - // Only spells cast, so no copies of spells - if ((stackObject instanceof Spell) - && !stackObject.isCopy() - && stackObject.isControlledBy(source.getControllerId())) { - Spell spell = (Spell) stackObject; - - if (FirstSpellCastWithTreasureCondition.instance.apply(game, source)) { - game.getState().addOtherAbility(spell.getCard(), cascadeAbility); - return true; // TODO: I think this should return here as soon as it finds the first one. - // If it should, change WildMageSorcerer to also return early. - } - } - } - return false; - } - - @Override - public RainOfRichesGainsCascadeEffect copy() { - return new RainOfRichesGainsCascadeEffect(this); - } -} - -enum FirstSpellCastWithTreasureCondition implements Condition { +enum RainOfRichesPredicate implements ObjectSourcePlayerPredicate { instance; @Override - public boolean apply(Game game, Ability source) { - if (game.getStack().isEmpty()) { + public boolean apply(ObjectSourcePlayer input, Game game) { + Permanent sourcePermanent = input.getSource().getSourcePermanentOrLKI(game); + if (sourcePermanent == null || !sourcePermanent.getControllerId().equals(input.getPlayerId())) { return false; } RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class); - StackObject so = game.getStack().getFirst(); - return watcher != null && RainOfRichesWatcher.checkSpell(so, game); + Card card = input.getObject(); + return watcher != null + && card instanceof StackObject + && watcher.checkSpell((Spell) card, game); } } @@ -127,7 +92,7 @@ class RainOfRichesWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { - if (event.getType() != GameEvent.EventType.CAST_SPELL) { + if (event.getType() != GameEvent.EventType.SPELL_CAST) { return; } Spell spell = game.getSpell(event.getSourceId()); @@ -148,13 +113,15 @@ class RainOfRichesWatcher extends Watcher { super.reset(); } - static boolean checkSpell(StackObject stackObject, Game game) { + boolean checkSpell(StackObject stackObject, Game game) { if (stackObject.isCopy() || !(stackObject instanceof Spell)) { return false; } - RainOfRichesWatcher watcher = game.getState().getWatcher(RainOfRichesWatcher.class); - return watcher.playerMap.containsKey(stackObject.getControllerId()) - && watcher.playerMap.get(stackObject.getControllerId()).refersTo(((Spell) stackObject).getMainCard(), game); + if (playerMap.containsKey(stackObject.getControllerId())) { + return playerMap.get(stackObject.getControllerId()).refersTo(((Spell) stackObject).getMainCard(), game); + } else { + return ManaPaidSourceWatcher.getTreasurePaid(stackObject.getId(), game) >= 1; + } } } 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 new file mode 100644 index 00000000000..2a65b1976db --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ncc/RainOfRichesTest.java @@ -0,0 +1,118 @@ +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; + +/** + * @author Susucr + */ +public class RainOfRichesTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.r.RainOfRiches Rain of Riches} {3}{R}{R} + * Enchantment + * When Rain of Riches enters the battlefield, create two Treasure tokens. + * The first spell you cast each turn that mana from a Treasure was spent to cast has cascade. + */ + private static final String rain = "Rain of Riches"; + + @Test + public void test_Using_Treasures() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, rain, 1); + addCard(Zone.HAND, playerA, "Goblin Piker", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.LIBRARY, playerA, "Elite Vanguard", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rain, true); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Goblin Piker"); + setChoice(playerA, "Red"); // choice for treasure mana + setChoice(playerA, "Red"); // choice for treasure mana + setChoice(playerA, true); // yes to Cascade + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Goblin Piker", 1); + assertPermanentCount(playerA, "Elite Vanguard", 1); + } + + @Test + public void test_Not_Using_Treasures() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, rain, 1); + addCard(Zone.HAND, playerA, "Goblin Piker", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 7); + addCard(Zone.LIBRARY, playerA, "Elite Vanguard", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rain, true); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Goblin Piker"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Goblin Piker", 1); + assertPermanentCount(playerA, "Elite Vanguard", 0); + } + + @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(); + + addCard(Zone.HAND, playerA, rain, 1); + addCard(Zone.HAND, playerA, "Raging Goblin", 2); // {R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 5); + addCard(Zone.LIBRARY, playerA, "Memnite", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rain, true); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Raging Goblin"); + setChoice(playerA, "Red"); // choice for treasure mana + setChoice(playerA, true); // yes to Cascade + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Raging Goblin"); + setChoice(playerA, "Red"); // choice for treasure mana + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Raging Goblin", 2); + assertPermanentCount(playerA, "Memnite", 1); + } + + @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); + + addCard(Zone.HAND, playerA, rain, 1); + addCard(Zone.HAND, playerA, "Raging Goblin", 2); // {R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 6); + addCard(Zone.LIBRARY, playerA, "Memnite", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, rain, true); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{T}: Add"); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Raging Goblin", true); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Raging Goblin"); + setChoice(playerA, "Red"); // choice for treasure mana + setChoice(playerA, true); // yes to Cascade + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Raging Goblin", 2); + assertPermanentCount(playerA, "Memnite", 1); + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java index d2243674730..7cef0dc05df 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainAbilityControlledSpellsEffect.java @@ -45,22 +45,22 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl { } for (Card card : game.getExile().getAllCardsByRange(game, source.getControllerId())) { - if (filter.match(card, game)) { + if (filter.match(card, player.getId(), source, game)) { game.getState().addOtherAbility(card, ability); } } for (Card card : player.getLibrary().getCards(game)) { - if (filter.match(card, game)) { + if (filter.match(card, player.getId(), source, game)) { game.getState().addOtherAbility(card, ability); } } for (Card card : player.getHand().getCards(game)) { - if (filter.match(card, game)) { + if (filter.match(card, player.getId(), source, game)) { game.getState().addOtherAbility(card, ability); } } for (Card card : player.getGraveyard().getCards(game)) { - if (filter.match(card, game)) { + if (filter.match(card, player.getId(), source, game)) { game.getState().addOtherAbility(card, ability); } } @@ -68,7 +68,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl { // workaround to gain cost reduction abilities to commanders before cast (make it playable) game.getCommanderCardsFromCommandZone(player, CommanderCardType.ANY) .stream() - .filter(card -> filter.match(card, game)) + .filter(card -> filter.match(card, player.getId(), source, game)) .forEach(card -> game.getState().addOtherAbility(card, ability)); for (StackObject stackObject : game.getStack()) { @@ -77,7 +77,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl { } // TODO: Distinguish "you cast" to exclude copies Card card = game.getCard(stackObject.getSourceId()); - if (card != null && filter.match((Spell) stackObject, game)) { + if (card != null && filter.match((Spell) stackObject, player.getId(), source, game)) { game.getState().addOtherAbility(card, ability); } } From 6eac890ca3366cd9ef3b880665cf216d4b21aacd Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:26:30 -0500 Subject: [PATCH 66/69] fix #13397 (Webstrike Elite) close #13401 --- Mage.Sets/src/mage/cards/w/WebstrikeElite.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/w/WebstrikeElite.java b/Mage.Sets/src/mage/cards/w/WebstrikeElite.java index bfbe9a00a7b..bc5997fa765 100644 --- a/Mage.Sets/src/mage/cards/w/WebstrikeElite.java +++ b/Mage.Sets/src/mage/cards/w/WebstrikeElite.java @@ -4,6 +4,7 @@ import mage.MageInt; import mage.abilities.common.ZoneChangeTriggeredAbility; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.dynamicvalue.common.GetXValue; +import mage.abilities.effects.common.DestroyTargetEffect; import mage.abilities.keyword.CyclingAbility; import mage.abilities.keyword.ReachAbility; import mage.cards.CardImpl; @@ -58,7 +59,7 @@ public final class WebstrikeElite extends CardImpl { class WebstrikeEliteTriggeredAbility extends ZoneChangeTriggeredAbility { WebstrikeEliteTriggeredAbility() { - super(Zone.ALL, null, "", false); + super(Zone.ALL, new DestroyTargetEffect(), "", false); } private WebstrikeEliteTriggeredAbility(final WebstrikeEliteTriggeredAbility ability) { From af8dc98acbce2418b126716d0310ad784fe77ff6 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:28:14 -0500 Subject: [PATCH 67/69] fix #13400 (Gastal Thrillroller) --- Mage.Sets/src/mage/cards/g/GastalThrillroller.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/g/GastalThrillroller.java b/Mage.Sets/src/mage/cards/g/GastalThrillroller.java index 3b8467276b3..8b1cf5da371 100644 --- a/Mage.Sets/src/mage/cards/g/GastalThrillroller.java +++ b/Mage.Sets/src/mage/cards/g/GastalThrillroller.java @@ -16,6 +16,7 @@ import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Duration; import mage.constants.SubType; +import mage.constants.Zone; import mage.counters.CounterType; import java.util.UUID; @@ -48,7 +49,7 @@ public final class GastalThrillroller extends CardImpl { // {2}{R}, Discard a card: Return this card from your graveyard to the battlefield with a finality counter on it. Activate only as a sorcery. Ability ability = new ActivateAsSorceryActivatedAbility( - new ReturnSourceFromGraveyardToBattlefieldWithCounterEffect( + Zone.GRAVEYARD, new ReturnSourceFromGraveyardToBattlefieldWithCounterEffect( CounterType.FINALITY.createInstance(), false ), new ManaCostsImpl<>("{2}{R}") ); From 35736af39dbd9a8540b51c6fa54ffb4ea47a39f1 Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:29:36 -0500 Subject: [PATCH 68/69] fix #13365 (Not Dead After All) --- Mage.Sets/src/mage/cards/n/NotDeadAfterAll.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/n/NotDeadAfterAll.java b/Mage.Sets/src/mage/cards/n/NotDeadAfterAll.java index 43917c6c726..9d58d02de3a 100644 --- a/Mage.Sets/src/mage/cards/n/NotDeadAfterAll.java +++ b/Mage.Sets/src/mage/cards/n/NotDeadAfterAll.java @@ -75,7 +75,7 @@ class NotDeadAfterAllEffect extends OneShotEffect { return false; } - player.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, false, null); + player.moveCards(card, Zone.BATTLEFIELD, source, game, true, false, true, null); Permanent permanent = game.getPermanent(card.getId()); if (permanent == null) { return false; From 37fc1737011156f9f431fd4c112c654a96d762ff Mon Sep 17 00:00:00 2001 From: xenohedron <12538125+xenohedron@users.noreply.github.com> Date: Sat, 1 Mar 2025 20:41:28 -0500 Subject: [PATCH 69/69] fix #13307 (Volatile Stormdrake) --- Mage.Sets/src/mage/cards/v/VolatileStormdrake.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Mage.Sets/src/mage/cards/v/VolatileStormdrake.java b/Mage.Sets/src/mage/cards/v/VolatileStormdrake.java index b4a160e66f4..52d1482ec65 100644 --- a/Mage.Sets/src/mage/cards/v/VolatileStormdrake.java +++ b/Mage.Sets/src/mage/cards/v/VolatileStormdrake.java @@ -4,11 +4,10 @@ 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; @@ -138,10 +137,13 @@ class VolatileStormdrakeEffect extends OneShotEffect { game.addEffect(effect, source); game.processAction(); controller.addCounters(CounterType.ENERGY.createInstance(4), controller.getId(), source, game); - new DoIfCostPaid( - null, new SacrificeTargetEffect("", controller.getId()), - new PayEnergyCost(targetPermanent.getManaValue()), true - ).apply(game, source); + 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); return true; } }