From 855bfabf24301bc15df01e5d67062b1133b3657e Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 15 Aug 2025 14:07:15 -0400 Subject: [PATCH 01/31] [TLA] Implement How to Start a Riot --- .../src/mage/cards/h/HowToStartARiot.java | 84 +++++++++++++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 1 + 2 files changed, 85 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HowToStartARiot.java diff --git a/Mage.Sets/src/mage/cards/h/HowToStartARiot.java b/Mage.Sets/src/mage/cards/h/HowToStartARiot.java new file mode 100644 index 00000000000..7c93cc5ea8c --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HowToStartARiot.java @@ -0,0 +1,84 @@ +package mage.cards.h; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.keyword.MenaceAbility; +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.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.ControllerIdPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.target.TargetPlayer; +import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.SecondTargetPointer; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HowToStartARiot extends CardImpl { + + public HowToStartARiot(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{R}"); + + this.subtype.add(SubType.LESSON); + + // Target creature gains menace until end of turn. + this.getSpellAbility().addEffect(new GainAbilityTargetEffect(new MenaceAbility(false))); + this.getSpellAbility().addTarget(new TargetCreaturePermanent()); + + // Creatures target player controls get +2/+0 until end of turn. + this.getSpellAbility().addEffect(new HowToStartARiotEffect()); + this.getSpellAbility().addTarget(new TargetPlayer()); + } + + private HowToStartARiot(final HowToStartARiot card) { + super(card); + } + + @Override + public HowToStartARiot copy() { + return new HowToStartARiot(this); + } +} + +class HowToStartARiotEffect extends OneShotEffect { + + HowToStartARiotEffect() { + super(Outcome.Benefit); + this.setTargetPointer(new SecondTargetPointer()); + staticText = "creatures target player controls get +2/+0 until end of turn"; + this.concatBy("
"); + } + + private HowToStartARiotEffect(final HowToStartARiotEffect effect) { + super(effect); + } + + @Override + public HowToStartARiotEffect copy() { + return new HowToStartARiotEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(getTargetPointer().getFirst(game, source)); + if (player == null) { + return false; + } + FilterCreaturePermanent filter = new FilterCreaturePermanent(); + filter.add(new ControllerIdPredicate(player.getId())); + game.addEffect(new BoostAllEffect( + 2, 0, Duration.EndOfTurn, filter, false + ), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 363d71b4765..0741808815e 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -71,6 +71,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Heartless Act", 103, Rarity.UNCOMMON, mage.cards.h.HeartlessAct.class)); cards.add(new SetCardInfo("Hei Bai, Spirit of Balance", 225, Rarity.UNCOMMON, mage.cards.h.HeiBaiSpiritOfBalance.class)); cards.add(new SetCardInfo("Hog-Monkey", 104, Rarity.COMMON, mage.cards.h.HogMonkey.class)); + cards.add(new SetCardInfo("How to Start a Riot", 140, Rarity.COMMON, mage.cards.h.HowToStartARiot.class)); cards.add(new SetCardInfo("Iguana Parrot", 56, Rarity.COMMON, mage.cards.i.IguanaParrot.class)); cards.add(new SetCardInfo("Island", 288, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("It'll Quench Ya!", 58, Rarity.COMMON, mage.cards.i.ItllQuenchYa.class)); From 5a78f79f32a8d47341c89bd393247610b6cfbe8c Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 15 Aug 2025 14:15:34 -0400 Subject: [PATCH 02/31] [TLA] Implement Serpent of the Pass --- .../src/mage/cards/s/SerpentOfThePass.java | 71 +++++++++++++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 1 + 2 files changed, 72 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SerpentOfThePass.java diff --git a/Mage.Sets/src/mage/cards/s/SerpentOfThePass.java b/Mage.Sets/src/mage/cards/s/SerpentOfThePass.java new file mode 100644 index 00000000000..a868ebce450 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SerpentOfThePass.java @@ -0,0 +1,71 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.condition.common.CardsInControllerGraveyardCondition; +import mage.abilities.decorator.ConditionalAsThoughEffect; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.dynamicvalue.common.CardsInControllerGraveyardCount; +import mage.abilities.effects.common.continuous.CastAsThoughItHadFlashSourceEffect; +import mage.abilities.effects.common.cost.SpellCostReductionForEachSourceEffect; +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.Duration; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.predicate.Predicates; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SerpentOfThePass extends CardImpl { + + private static final Condition condition = new CardsInControllerGraveyardCondition(3, new FilterCard(SubType.LESSON)); + private static final Hint hint = new ValueHint( + "Lesson cards in your graveyard", new CardsInControllerGraveyardCount(new FilterCard(SubType.LESSON)) + ); + private static final FilterCard filter = new FilterCard("noncreature, nonland card"); + + static { + filter.add(Predicates.not(CardType.CREATURE.getPredicate())); + filter.add(Predicates.not(CardType.LAND.getPredicate())); + } + + private static final DynamicValue xValue = new CardsInControllerGraveyardCount(filter); + private static final Hint hint2 = new ValueHint("Noncreature, nonland cards in your graveyard", xValue); + + public SerpentOfThePass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{5}{U}{U}"); + + this.subtype.add(SubType.SERPENT); + this.power = new MageInt(6); + this.toughness = new MageInt(5); + + // If there are three or more Lesson cards in your graveyard, you may cast this spell as though it had flash. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new ConditionalAsThoughEffect( + new CastAsThoughItHadFlashSourceEffect(Duration.EndOfGame), condition + ).setText("if there are three or more Lesson cards in your graveyard, " + + "you may cast this spell as though it had flash.")).addHint(hint)); + + // This spell costs {1} less to cast for each noncreature, nonland card in your graveyard. + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new SpellCostReductionForEachSourceEffect(1, xValue) + ).setRuleAtTheTop(true).addHint(hint2)); + } + + private SerpentOfThePass(final SerpentOfThePass card) { + super(card); + } + + @Override + public SerpentOfThePass copy() { + return new SerpentOfThePass(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 0741808815e..85f484eb4c1 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -104,6 +104,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Rough Rhino Cavalry", 152, Rarity.COMMON, mage.cards.r.RoughRhinoCavalry.class)); cards.add(new SetCardInfo("Rowdy Snowballers", 68, Rarity.COMMON, mage.cards.r.RowdySnowballers.class)); cards.add(new SetCardInfo("Saber-Tooth Moose-Lion", 194, Rarity.COMMON, mage.cards.s.SaberToothMooseLion.class)); + cards.add(new SetCardInfo("Serpent of the Pass", 70, Rarity.UNCOMMON, mage.cards.s.SerpentOfThePass.class)); cards.add(new SetCardInfo("Sokka's Haiku", 71, Rarity.UNCOMMON, mage.cards.s.SokkasHaiku.class)); cards.add(new SetCardInfo("Sokka, Bold Boomeranger", 240, Rarity.RARE, mage.cards.s.SokkaBoldBoomeranger.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sokka, Bold Boomeranger", 383, Rarity.RARE, mage.cards.s.SokkaBoldBoomeranger.class, NON_FULL_USE_VARIOUS)); From c28facf759349387e55b768deae0254ab29d7c83 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 15 Aug 2025 14:20:51 -0400 Subject: [PATCH 03/31] [TLA] Implement Long Feng, Grand Secretariat --- .../cards/l/LongFengGrandSecretariat.java | 72 +++++++++++++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 1 + 2 files changed, 73 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/l/LongFengGrandSecretariat.java diff --git a/Mage.Sets/src/mage/cards/l/LongFengGrandSecretariat.java b/Mage.Sets/src/mage/cards/l/LongFengGrandSecretariat.java new file mode 100644 index 00000000000..b3d6d812afb --- /dev/null +++ b/Mage.Sets/src/mage/cards/l/LongFengGrandSecretariat.java @@ -0,0 +1,72 @@ +package mage.cards.l; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.PutIntoGraveFromBattlefieldAllTriggeredAbility; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.counters.CounterType; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.ObjectSourcePlayer; +import mage.filter.predicate.ObjectSourcePlayerPredicate; +import mage.filter.predicate.mageobject.AnotherPredicate; +import mage.game.Game; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class LongFengGrandSecretariat extends CardImpl { + + private static final FilterPermanent filter = new FilterControlledPermanent("another creature you control or a land you control"); + + static { + filter.add(LongFengGrandSecretariatPredicate.instance); + } + + public LongFengGrandSecretariat(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{B/G}{B/G}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ADVISOR); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Whenever another creature you control or a land you control is put into a graveyard from the battlefield, put a +1/+1 counter on target creature you control. + Ability ability = new PutIntoGraveFromBattlefieldAllTriggeredAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance()), + false, filter, false + ); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(ability); + } + + private LongFengGrandSecretariat(final LongFengGrandSecretariat card) { + super(card); + } + + @Override + public LongFengGrandSecretariat copy() { + return new LongFengGrandSecretariat(this); + } +} + +enum LongFengGrandSecretariatPredicate implements ObjectSourcePlayerPredicate { + instance; + + @Override + public boolean apply(ObjectSourcePlayer input, Game game) { + return input.getObject().isLand(game) + || input.getObject().isCreature(game) + && AnotherPredicate.instance.apply(input, game); + } +} diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 85f484eb4c1..75e7e8bfda9 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -83,6 +83,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Katara, the Fearless", 350, Rarity.RARE, mage.cards.k.KataraTheFearless.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Katara, the Fearless", 361, Rarity.RARE, mage.cards.k.KataraTheFearless.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Lightning Strike", 146, Rarity.COMMON, mage.cards.l.LightningStrike.class)); + cards.add(new SetCardInfo("Long Feng, Grand Secretariat", 233, Rarity.UNCOMMON, mage.cards.l.LongFengGrandSecretariat.class)); cards.add(new SetCardInfo("Master Pakku", 63, Rarity.UNCOMMON, mage.cards.m.MasterPakku.class)); cards.add(new SetCardInfo("Master Piandao", 28, Rarity.UNCOMMON, mage.cards.m.MasterPiandao.class)); cards.add(new SetCardInfo("Merchant of Many Hats", 110, Rarity.COMMON, mage.cards.m.MerchantOfManyHats.class)); From ea2dca64103aacd7976c895786d7d19c1f293f8c Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 15 Aug 2025 14:23:01 -0400 Subject: [PATCH 04/31] [TLA] Implement Sokka, Lateral Strategist --- .../mage/cards/s/SokkaLateralStrategist.java | 75 +++++++++++++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 1 + 2 files changed, 76 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java diff --git a/Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java b/Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java new file mode 100644 index 00000000000..d0808b22dfa --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java @@ -0,0 +1,75 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class SokkaLateralStrategist extends CardImpl { + + public SokkaLateralStrategist(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{W/U}{W/U}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(2); + this.toughness = new MageInt(4); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Whenever Sokka and at least one other creature attack, draw a card. + this.addAbility(new SokkaLateralStrategistTriggeredAbility()); + } + + private SokkaLateralStrategist(final SokkaLateralStrategist card) { + super(card); + } + + @Override + public SokkaLateralStrategist copy() { + return new SokkaLateralStrategist(this); + } +} + +class SokkaLateralStrategistTriggeredAbility extends TriggeredAbilityImpl { + + SokkaLateralStrategistTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect()); + this.setTriggerPhrase("Whenever {this} and at least one other creature attack, "); + } + + private SokkaLateralStrategistTriggeredAbility(final SokkaLateralStrategistTriggeredAbility ability) { + super(ability); + } + + @Override + public SokkaLateralStrategistTriggeredAbility copy() { + return new SokkaLateralStrategistTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DECLARED_ATTACKERS; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return game.getCombat().getAttackers().size() >= 2 && game.getCombat().getAttackers().contains(getSourceId()); + } +} diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 75e7e8bfda9..28effa48995 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -109,6 +109,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Sokka's Haiku", 71, Rarity.UNCOMMON, mage.cards.s.SokkasHaiku.class)); cards.add(new SetCardInfo("Sokka, Bold Boomeranger", 240, Rarity.RARE, mage.cards.s.SokkaBoldBoomeranger.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Sokka, Bold Boomeranger", 383, Rarity.RARE, mage.cards.s.SokkaBoldBoomeranger.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sokka, Lateral Strategist", 241, Rarity.UNCOMMON, mage.cards.s.SokkaLateralStrategist.class)); cards.add(new SetCardInfo("Southern Air Temple", 36, Rarity.UNCOMMON, mage.cards.s.SouthernAirTemple.class)); cards.add(new SetCardInfo("Suki, Kyoshi Warrior", 243, Rarity.UNCOMMON, mage.cards.s.SukiKyoshiWarrior.class)); cards.add(new SetCardInfo("Swamp", 289, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); From 96dbfc757ef952a252e739c535e8cf9916bd1ae8 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 15 Aug 2025 14:40:57 -0400 Subject: [PATCH 05/31] fix error --- Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java b/Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java index d0808b22dfa..82338117d3b 100644 --- a/Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java +++ b/Mage.Sets/src/mage/cards/s/SokkaLateralStrategist.java @@ -50,7 +50,7 @@ public final class SokkaLateralStrategist extends CardImpl { class SokkaLateralStrategistTriggeredAbility extends TriggeredAbilityImpl { SokkaLateralStrategistTriggeredAbility() { - super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect()); + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1)); this.setTriggerPhrase("Whenever {this} and at least one other creature attack, "); } From b64f1dce45fc486d85e894c2a7e165c1f1485884 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Fri, 15 Aug 2025 16:37:55 -0400 Subject: [PATCH 06/31] [TLA] Implement Razor Rings, rework excess damage (#13910) * [TLA] Implement Razor Rings * add overflow protection --- .../src/mage/cards/b/BottleCapBlast.java | 11 ++- .../src/mage/cards/c/ContestOfClaws.java | 40 ++++------- .../src/mage/cards/g/GoblinNegotiation.java | 15 ++-- Mage.Sets/src/mage/cards/h/HellToPay.java | 12 ++-- Mage.Sets/src/mage/cards/l/LacerateFlesh.java | 9 ++- .../cards/m/MegatronDestructiveForce.java | 26 ++----- .../src/mage/cards/n/NahirisWarcrafting.java | 5 +- Mage.Sets/src/mage/cards/o/OrbitalPlunge.java | 7 +- Mage.Sets/src/mage/cards/r/RamThrough.java | 46 ++++++------ Mage.Sets/src/mage/cards/r/RazorRings.java | 72 +++++++++++++++++++ .../src/mage/cards/t/TorchTheWitness.java | 3 +- .../src/mage/cards/u/UnleashTheInferno.java | 23 +++--- .../mage/cards/v/VikyaScorchingStalwart.java | 15 ++-- .../src/mage/cards/w/WindswiftSlice.java | 43 +++++------ .../src/mage/sets/AvatarTheLastAirbender.java | 1 + .../common/DamageWithExcessEffect.java | 16 +++-- .../java/mage/game/permanent/Permanent.java | 18 +++++ 17 files changed, 209 insertions(+), 153 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/r/RazorRings.java diff --git a/Mage.Sets/src/mage/cards/b/BottleCapBlast.java b/Mage.Sets/src/mage/cards/b/BottleCapBlast.java index 6d9d536f495..1b19927a649 100644 --- a/Mage.Sets/src/mage/cards/b/BottleCapBlast.java +++ b/Mage.Sets/src/mage/cards/b/BottleCapBlast.java @@ -62,8 +62,8 @@ class BottleCapBlastEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - UUID target = getTargetPointer().getFirst(game, source); - Player player = game.getPlayer(target); + UUID targetId = getTargetPointer().getFirst(game, source); + Player player = game.getPlayer(targetId); if (player != null) { player.damage(5, source, game); return true; @@ -72,11 +72,10 @@ class BottleCapBlastEffect extends OneShotEffect { if (permanent == null) { return false; } - int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), 5); - permanent.damage(5, source.getSourceId(), source, game); - if (lethal < 5) { + int excess = permanent.damageWithExcess(5, source, game); + if (excess > 0) { new TreasureToken().putOntoBattlefield( - 5 - lethal, game, source, source.getControllerId(), true, false + excess, game, source, source.getControllerId(), true, false ); } return true; diff --git a/Mage.Sets/src/mage/cards/c/ContestOfClaws.java b/Mage.Sets/src/mage/cards/c/ContestOfClaws.java index 530b4a9847e..afa5f2dc6d4 100644 --- a/Mage.Sets/src/mage/cards/c/ContestOfClaws.java +++ b/Mage.Sets/src/mage/cards/c/ContestOfClaws.java @@ -1,7 +1,5 @@ package mage.cards.c; -import java.util.UUID; - import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.keyword.DiscoverEffect; @@ -9,34 +7,29 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; -import mage.filter.StaticFilters; +import mage.game.Controllable; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.Player; -import mage.target.Target; import mage.target.TargetPermanent; import mage.target.common.TargetControlledCreaturePermanent; -import mage.target.common.TargetCreaturePermanent; + +import java.util.Optional; +import java.util.UUID; import static mage.filter.StaticFilters.FILTER_ANOTHER_CREATURE_TARGET_2; /** - * * @author jimga150 */ public final class ContestOfClaws extends CardImpl { public ContestOfClaws(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{G}"); - // Target creature you control deals damage equal to its power to another target creature. If excess damage was dealt this way, discover X, where X is that excess damage. this.getSpellAbility().addEffect(new ContestOfClawsDamageEffect()); - Target target = new TargetControlledCreaturePermanent().setTargetTag(1); - this.getSpellAbility().addTarget(target); - - Target target2 = new TargetPermanent(FILTER_ANOTHER_CREATURE_TARGET_2).setTargetTag(2); - this.getSpellAbility().addTarget(target2); + this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent().setTargetTag(1)); + this.getSpellAbility().addTarget(new TargetPermanent(FILTER_ANOTHER_CREATURE_TARGET_2).setTargetTag(2)); } private ContestOfClaws(final ContestOfClaws card) { @@ -74,22 +67,13 @@ class ContestOfClawsDamageEffect extends OneShotEffect { if (ownCreature == null || targetCreature == null) { return false; } - int damage = ownCreature.getPower().getValue(); - int lethalDamage = targetCreature.getLethalDamage(source.getSourceId(), game); - targetCreature.damage(damage, ownCreature.getId(), source, game, false, true); - - if (damage < lethalDamage){ - return true; + int excess = targetCreature.damageWithExcess( ownCreature.getPower().getValue(), ownCreature.getId(), source, game); + if (excess > 0) { + Optional.ofNullable(source) + .map(Controllable::getControllerId) + .map(game::getPlayer) + .ifPresent(player -> DiscoverEffect.doDiscover(player, excess, game, source)); } - int discoverValue = damage - lethalDamage; - Player player = game.getPlayer(source.getControllerId()); - - if (player == null){ - // If somehow this case is hit, the damage still technically happened, so i guess it applied? - return true; - } - DiscoverEffect.doDiscover(player, discoverValue, game, source); - return true; } } diff --git a/Mage.Sets/src/mage/cards/g/GoblinNegotiation.java b/Mage.Sets/src/mage/cards/g/GoblinNegotiation.java index 368eb608eab..538d9976e60 100644 --- a/Mage.Sets/src/mage/cards/g/GoblinNegotiation.java +++ b/Mage.Sets/src/mage/cards/g/GoblinNegotiation.java @@ -15,7 +15,6 @@ import mage.util.CardUtil; import java.util.UUID; /** - * * @author ciaccona007 */ public final class GoblinNegotiation extends CardImpl { @@ -62,14 +61,12 @@ class GoblinNegotiationEffect extends OneShotEffect { if (permanent == null) { return false; } - int damage = CardUtil.getSourceCostsTag(game, source, "X", 0); - int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), damage); - permanent.damage(damage, source.getSourceId(), source, game); - if (damage > lethal) { - new GoblinToken().putOntoBattlefield( - damage - lethal, game, source, source.getControllerId() - ); + int excess = permanent.damageWithExcess( + CardUtil.getSourceCostsTag(game, source, "X", 0), source, game + ); + if (excess > 0) { + new GoblinToken().putOntoBattlefield(excess, game, source); } return true; } -} \ No newline at end of file +} diff --git a/Mage.Sets/src/mage/cards/h/HellToPay.java b/Mage.Sets/src/mage/cards/h/HellToPay.java index 640b2be38b8..8b3e35e9589 100644 --- a/Mage.Sets/src/mage/cards/h/HellToPay.java +++ b/Mage.Sets/src/mage/cards/h/HellToPay.java @@ -60,13 +60,11 @@ class HellToPayEffect extends OneShotEffect { if (permanent == null) { return false; } - int damage = CardUtil.getSourceCostsTag(game, source, "X", 0); - int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), damage); - permanent.damage(damage, source.getSourceId(), source, game); - if (damage > lethal) { - new TreasureToken().putOntoBattlefield( - damage - lethal, game, source, source.getControllerId(), true, false - ); + int excess = permanent.damageWithExcess( + CardUtil.getSourceCostsTag(game, source, "X", 0), source, game + ); + if (excess > 0) { + new TreasureToken().putOntoBattlefield(excess, game, source, source.getControllerId(), true, false); } return true; } diff --git a/Mage.Sets/src/mage/cards/l/LacerateFlesh.java b/Mage.Sets/src/mage/cards/l/LacerateFlesh.java index 0d4f8c4eea3..9e060e65cd7 100644 --- a/Mage.Sets/src/mage/cards/l/LacerateFlesh.java +++ b/Mage.Sets/src/mage/cards/l/LacerateFlesh.java @@ -55,14 +55,13 @@ class LacerateFleshEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Permanent permanent = game.getPermanent(source.getFirstTarget()); + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); if (permanent == null) { return false; } - int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), 4); - permanent.damage(4, source.getSourceId(), source, game); - if (lethal < 4) { - new BloodToken().putOntoBattlefield(4 - lethal, game, source); + int excess = permanent.damageWithExcess(4, source, game); + if (excess > 0) { + new BloodToken().putOntoBattlefield(excess, game, source); } return true; } diff --git a/Mage.Sets/src/mage/cards/m/MegatronDestructiveForce.java b/Mage.Sets/src/mage/cards/m/MegatronDestructiveForce.java index 2badfb10257..7b718d6efbd 100644 --- a/Mage.Sets/src/mage/cards/m/MegatronDestructiveForce.java +++ b/Mage.Sets/src/mage/cards/m/MegatronDestructiveForce.java @@ -79,7 +79,7 @@ class MegatronDestructiveForceEffect extends OneShotEffect { return false; } TargetSacrifice target = new TargetSacrifice( - 0, 1, StaticFilters.FILTER_CONTROLLED_ANOTHER_ARTIFACT + 0, 1, StaticFilters.FILTER_CONTROLLED_ANOTHER_ARTIFACT ); player.choose(outcome, target, source, game); Permanent permanent = game.getPermanent(target.getFirstTarget()); @@ -93,7 +93,7 @@ class MegatronDestructiveForceEffect extends OneShotEffect { } ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility( - new MegatronDestructiveForceReflexiveEffect(manaValue), false + new MegatronDestructiveForceReflexiveEffect(manaValue), false ); ability.addHint(new StaticHint("Sacrificed artifact mana value: " + manaValue)); ability.addTarget(new TargetCreaturePermanent()); @@ -109,8 +109,8 @@ class MegatronDestructiveForceReflexiveEffect extends OneShotEffect { MegatronDestructiveForceReflexiveEffect(int value) { super(Outcome.Damage); staticText = "{this} deals damage equal to the sacrificed artifact's mana value to target " + - "creature. If excess damage would be dealt to that creature this way, instead that damage " + - "is dealt to that creature's controller and you convert {this}."; + "creature. If excess damage would be dealt to that creature this way, instead that damage " + + "is dealt to that creature's controller and you convert {this}."; this.value = value; } @@ -126,36 +126,22 @@ class MegatronDestructiveForceReflexiveEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Permanent sourcePermanent = source.getSourcePermanentOrLKI(game); - if (sourcePermanent == null) { - return false; - } - if (value < 1) { return false; } - Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); if (permanent == null) { return false; } - - int lethal = permanent.getLethalDamage(source.getSourceId(), game); - int excess = value - lethal; - if (excess <= 0) { - // no excess damage. - permanent.damage(value, source.getSourceId(), source, game); + int excess = permanent.damageWithExcess(value, source, game); + if (excess < 1) { return true; } - - // excess damage. dealing excess to controller's instead. And convert Megatron. - permanent.damage(lethal, source.getSourceId(), source, game); Player player = game.getPlayer(permanent.getControllerId()); if (player != null) { player.damage(excess, source, game); } new TransformSourceEffect().apply(game, source); - return true; } } diff --git a/Mage.Sets/src/mage/cards/n/NahirisWarcrafting.java b/Mage.Sets/src/mage/cards/n/NahirisWarcrafting.java index cd5f2e4ceab..c170893bc2d 100644 --- a/Mage.Sets/src/mage/cards/n/NahirisWarcrafting.java +++ b/Mage.Sets/src/mage/cards/n/NahirisWarcrafting.java @@ -80,9 +80,8 @@ class NahirisWarcraftingEffect extends OneShotEffect { if (player == null || permanent == null) { return false; } - int lethal = permanent.getLethalDamage(source.getSourceId(), game); - int excess = permanent.damage(5, source, game) - lethal; - if (excess <= 0) { + int excess = permanent.damageWithExcess(5, source, game); + if (excess < 1) { return true; } Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, excess)); diff --git a/Mage.Sets/src/mage/cards/o/OrbitalPlunge.java b/Mage.Sets/src/mage/cards/o/OrbitalPlunge.java index e5238585a3d..a7436dcb122 100644 --- a/Mage.Sets/src/mage/cards/o/OrbitalPlunge.java +++ b/Mage.Sets/src/mage/cards/o/OrbitalPlunge.java @@ -2,7 +2,6 @@ package mage.cards.o; import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.CreateTokenEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -61,10 +60,8 @@ class OrbitalPlungeEffect extends OneShotEffect { if (permanent == null) { return false; } - int lethal = permanent.getLethalDamage(source.getSourceId(), game); - permanent.damage(6, source.getSourceId(), source, game); - if (lethal < 6) { - new CreateTokenEffect(new LanderToken()).apply(game, source); + if (permanent.damageWithExcess(6, source, game) > 0) { + new LanderToken().putOntoBattlefield(1, game, source); } return true; } diff --git a/Mage.Sets/src/mage/cards/r/RamThrough.java b/Mage.Sets/src/mage/cards/r/RamThrough.java index 582f0ccb988..18bac9209d1 100644 --- a/Mage.Sets/src/mage/cards/r/RamThrough.java +++ b/Mage.Sets/src/mage/cards/r/RamThrough.java @@ -7,15 +7,18 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; -import mage.filter.StaticFilters; +import mage.game.Controllable; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.Player; import mage.target.TargetPermanent; import mage.target.common.TargetControlledCreaturePermanent; -import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.EachTargetPointer; +import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; import static mage.filter.StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL; @@ -47,6 +50,7 @@ class RamThroughEffect extends OneShotEffect { RamThroughEffect() { super(Outcome.Benefit); + this.setTargetPointer(new EachTargetPointer()); staticText = "Target creature you control deals damage equal to its power to target creature you don't control. " + "If the creature you control has trample, excess damage is dealt to that creature's controller instead."; } @@ -62,29 +66,31 @@ class RamThroughEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - if (source.getTargets().size() != 2) { - throw new IllegalStateException("It must have two targets, but found " + source.getTargets().size()); - } - - Permanent myPermanent = game.getPermanent(getTargetPointer().getFirst(game, source)); - Permanent anotherPermanent = game.getPermanent(source.getTargets().get(1).getFirstTarget()); - - if (myPermanent == null || anotherPermanent == null) { + List permanents = this + .getTargetPointer() + .getTargets(game, source) + .stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (permanents.size() < 2) { return false; } - int power = myPermanent.getPower().getValue(); + Permanent permanent = permanents.get(0); + int power = permanent.getPower().getValue(); if (power < 1) { return false; } - if (!myPermanent.getAbilities().containsKey(TrampleAbility.getInstance().getId())) { - return anotherPermanent.damage(power, myPermanent.getId(), source, game, false, true) > 0; + Permanent creature = permanents.get(1); + if (!permanent.hasAbility(TrampleAbility.getInstance(), game)) { + return creature.damage(power, permanent.getId(), source, game) > 0; } - int lethal = anotherPermanent.getLethalDamage(myPermanent.getId(), game); - lethal = Math.min(lethal, power); - anotherPermanent.damage(lethal, myPermanent.getId(), source, game); - Player player = game.getPlayer(anotherPermanent.getControllerId()); - if (player != null && lethal < power) { - player.damage(power - lethal, myPermanent.getId(), source, game); + int excess = creature.damageWithExcess(power, permanent.getId(), source, game); + if (excess > 0) { + Optional.ofNullable(creature) + .map(Controllable::getControllerId) + .map(game::getPlayer) + .ifPresent(player -> player.damage(excess, permanent.getId(), source, game)); } return true; } diff --git a/Mage.Sets/src/mage/cards/r/RazorRings.java b/Mage.Sets/src/mage/cards/r/RazorRings.java new file mode 100644 index 00000000000..de184c5311d --- /dev/null +++ b/Mage.Sets/src/mage/cards/r/RazorRings.java @@ -0,0 +1,72 @@ +package mage.cards.r; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.game.Controllable; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetAttackingOrBlockingCreature; + +import java.util.Optional; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class RazorRings extends CardImpl { + + public RazorRings(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}"); + + // Razor Rings deals 4 damage to target attacking or blocking creature. You gain life equal to the excess damage dealt this way. + this.getSpellAbility().addEffect(new RazorRingsEffect()); + this.getSpellAbility().addTarget(new TargetAttackingOrBlockingCreature()); + } + + private RazorRings(final RazorRings card) { + super(card); + } + + @Override + public RazorRings copy() { + return new RazorRings(this); + } +} + +class RazorRingsEffect extends OneShotEffect { + + RazorRingsEffect() { + super(Outcome.Benefit); + staticText = "{this} deals 4 damage to target attacking or blocking creature. " + + "You gain life equal to the excess damage dealt this way"; + } + + private RazorRingsEffect(final RazorRingsEffect effect) { + super(effect); + } + + @Override + public RazorRingsEffect copy() { + return new RazorRingsEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + int excess = permanent.damageWithExcess(4, source, game); + if (excess > 0) { + Optional.ofNullable(source) + .map(Controllable::getControllerId) + .map(game::getPlayer) + .ifPresent(player -> player.gainLife(excess, game, source)); + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/t/TorchTheWitness.java b/Mage.Sets/src/mage/cards/t/TorchTheWitness.java index 283c3409be3..da1d98ac6e1 100644 --- a/Mage.Sets/src/mage/cards/t/TorchTheWitness.java +++ b/Mage.Sets/src/mage/cards/t/TorchTheWitness.java @@ -60,8 +60,7 @@ class TorchTheWitnessEffect extends OneShotEffect { if (permanent == null) { return false; } - int lethal = permanent.getLethalDamage(source.getSourceId(), game); - if (lethal < permanent.damage(2 * CardUtil.getSourceCostsTag(game, source, "X", 0), source, game)) { + if (permanent.damageWithExcess(2 * CardUtil.getSourceCostsTag(game, source, "X", 0), source, game) > 0) { InvestigateEffect.doInvestigate(source.getControllerId(), 1, game, source); } return true; diff --git a/Mage.Sets/src/mage/cards/u/UnleashTheInferno.java b/Mage.Sets/src/mage/cards/u/UnleashTheInferno.java index 33ccf27b203..a2df1217059 100644 --- a/Mage.Sets/src/mage/cards/u/UnleashTheInferno.java +++ b/Mage.Sets/src/mage/cards/u/UnleashTheInferno.java @@ -67,19 +67,18 @@ class UnleashTheInfernoEffect extends OneShotEffect { if (permanent == null) { return false; } - int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), 7); - permanent.damage(7, source, game); - int excess = 7 - lethal; - if (excess > 0) { - ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(new DestroyTargetEffect(), false); - FilterPermanent filter = new FilterArtifactOrEnchantmentPermanent( - "artifact or enchantment an opponent controls with mana value less than or equal to " + excess - ); - filter.add(TargetController.OPPONENT.getControllerPredicate()); - filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, excess + 1)); - ability.addTarget(new TargetPermanent(filter)); - game.fireReflexiveTriggeredAbility(ability, source); + int excess = permanent.damageWithExcess(7, source, game); + if (excess < 1) { + return true; } + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(new DestroyTargetEffect(), false); + FilterPermanent filter = new FilterArtifactOrEnchantmentPermanent( + "artifact or enchantment an opponent controls with mana value less than or equal to " + excess + ); + filter.add(TargetController.OPPONENT.getControllerPredicate()); + filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, excess + 1)); + ability.addTarget(new TargetPermanent(filter)); + game.fireReflexiveTriggeredAbility(ability, source); return true; } } diff --git a/Mage.Sets/src/mage/cards/v/VikyaScorchingStalwart.java b/Mage.Sets/src/mage/cards/v/VikyaScorchingStalwart.java index 8fa03b3c406..bd59aeb7c9b 100644 --- a/Mage.Sets/src/mage/cards/v/VikyaScorchingStalwart.java +++ b/Mage.Sets/src/mage/cards/v/VikyaScorchingStalwart.java @@ -14,11 +14,13 @@ import mage.constants.CardType; import mage.constants.Outcome; import mage.constants.SubType; import mage.constants.SuperType; +import mage.game.Controllable; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetAnyTarget; +import java.util.Optional; import java.util.UUID; /** @@ -91,14 +93,11 @@ class VikyaScorchingStalwartEffect extends OneShotEffect { if (!permanent.isCreature(game)) { return permanent.damage(amount, source, game) > 0; } - int lethal = permanent.getLethalDamage(source.getSourceId(), game); - permanent.damage(amount, source.getSourceId(), source, game); - if (lethal >= amount) { - return true; - } - Player player = game.getPlayer(source.getControllerId()); - if (player != null) { - player.drawCards(1, source, game); + if (permanent.damageWithExcess(amount, source, game) > 0) { + Optional.ofNullable(source) + .map(Controllable::getControllerId) + .map(game::getPlayer) + .ifPresent(player -> player.drawCards(1, source, game)); } return true; } diff --git a/Mage.Sets/src/mage/cards/w/WindswiftSlice.java b/Mage.Sets/src/mage/cards/w/WindswiftSlice.java index 0c9751e1c7a..59cb03ca713 100644 --- a/Mage.Sets/src/mage/cards/w/WindswiftSlice.java +++ b/Mage.Sets/src/mage/cards/w/WindswiftSlice.java @@ -1,32 +1,33 @@ package mage.cards.w; -import java.util.UUID; - import mage.abilities.Ability; import mage.abilities.effects.OneShotEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; -import mage.filter.StaticFilters; import mage.game.Game; import mage.game.permanent.Permanent; import mage.game.permanent.token.ElfWarriorToken; import mage.target.TargetPermanent; import mage.target.common.TargetControlledCreaturePermanent; -import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.EachTargetPointer; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; import static mage.filter.StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL; /** - * * @author bwsinger */ public final class WindswiftSlice extends CardImpl { public WindswiftSlice(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{2}{G}"); - + // Target creature you control deals damage equal to its power to target creature you don't control. Create a number of 1/1 green Elf Warrior creature tokens equal to the amount of excess damage dealt this way. this.getSpellAbility().addEffect(new WindswiftSliceEffect()); @@ -48,6 +49,7 @@ class WindswiftSliceEffect extends OneShotEffect { WindswiftSliceEffect() { super(Outcome.Benefit); + this.setTargetPointer(new EachTargetPointer()); staticText = "Target creature you control deals damage equal to its power to target " + "creature you don't control. Create a number of 1/1 green Elf Warrior creature " + "tokens equal to the amount of excess damage dealt this way."; @@ -64,27 +66,26 @@ class WindswiftSliceEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { - Permanent myPermanent = game.getPermanent(getTargetPointer().getFirst(game, source)); - Permanent anotherPermanent = game.getPermanent(source.getTargets().get(1).getFirstTarget()); - - if (myPermanent == null || anotherPermanent == null) { + List permanents = this + .getTargetPointer() + .getTargets(game, source) + .stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (permanents.size() < 2) { return false; } - - int power = myPermanent.getPower().getValue(); + Permanent permanent = permanents.get(0); + int power = permanent.getPower().getValue(); if (power < 1) { return false; } - - int lethal = anotherPermanent.getLethalDamage(myPermanent.getId(), game); - lethal = Math.min(lethal, power); - - anotherPermanent.damage(power, myPermanent.getId(), source, game); - - if (lethal < power) { - new ElfWarriorToken().putOntoBattlefield(power - lethal, game, source, source.getControllerId()); + Permanent creature = permanents.get(1); + int excess = creature.damageWithExcess(power, permanent.getId(), source, game); + if (excess > 0) { + new ElfWarriorToken().putOntoBattlefield(excess, game, source); } - return true; } } diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 28effa48995..4c3fd56748c 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -98,6 +98,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Pretending Poxbearers", 237, Rarity.COMMON, mage.cards.p.PretendingPoxbearers.class)); cards.add(new SetCardInfo("Rabaroo Troop", 32, Rarity.COMMON, mage.cards.r.RabarooTroop.class)); cards.add(new SetCardInfo("Raucous Audience", 190, Rarity.COMMON, mage.cards.r.RaucousAudience.class)); + cards.add(new SetCardInfo("Razor Rings", 33, Rarity.COMMON, mage.cards.r.RazorRings.class)); cards.add(new SetCardInfo("Rebellious Captives", 191, Rarity.COMMON, mage.cards.r.RebelliousCaptives.class)); cards.add(new SetCardInfo("Redirect Lightning", 151, Rarity.RARE, mage.cards.r.RedirectLightning.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Redirect Lightning", 343, Rarity.RARE, mage.cards.r.RedirectLightning.class, NON_FULL_USE_VARIOUS)); diff --git a/Mage/src/main/java/mage/abilities/effects/common/DamageWithExcessEffect.java b/Mage/src/main/java/mage/abilities/effects/common/DamageWithExcessEffect.java index 84721c6224f..2df5d3a11cd 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/DamageWithExcessEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/DamageWithExcessEffect.java @@ -6,9 +6,11 @@ import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.OneShotEffect; import mage.constants.Outcome; +import mage.game.Controllable; import mage.game.Game; import mage.game.permanent.Permanent; -import mage.players.Player; + +import java.util.Optional; /** * @author TheElk801 @@ -47,12 +49,12 @@ public class DamageWithExcessEffect extends OneShotEffect { return false; } int damage = amount.calculate(game, source, this); - int lethal = permanent.getLethalDamage(source.getSourceId(), game); - lethal = Math.min(lethal, damage); - permanent.damage(lethal, source.getSourceId(), source, game); - Player player = game.getPlayer(permanent.getControllerId()); - if (player != null && lethal < damage) { - player.damage(damage - lethal, source.getSourceId(), source, game); + int excess = permanent.damageWithExcess(damage, source, game); + if (excess > 0) { + Optional.ofNullable(permanent) + .map(Controllable::getControllerId) + .map(game::getPlayer) + .ifPresent(player -> player.damage(excess, source, game)); } return true; } diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 1447879b362..73075f8f663 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -8,6 +8,7 @@ import mage.constants.Zone; import mage.game.Controllable; import mage.game.Game; import mage.game.GameState; +import mage.util.CardUtil; import java.util.List; import java.util.Set; @@ -177,6 +178,23 @@ public interface Permanent extends Card, Controllable { int getLethalDamage(UUID attackerId, Game game); + /** + * Same arguments as regular damage method, but returns the amount of excess damage dealt instead + * + * @return + */ + default int damageWithExcess(int damage, Ability source, Game game) { + return this.damageWithExcess(damage, source.getSourceId(), source, game); + } + + default int damageWithExcess(int damage, UUID attackerId, Ability source, Game game) { + int lethal = getLethalDamage(attackerId, game); + int excess = Math.max(CardUtil.overflowDec(damage, lethal), 0); + int dealt = Math.min(lethal, damage); + this.damage(dealt, attackerId, source, game); + return excess; + } + void removeAllDamage(Game game); void reset(Game game); From b470f0b679708cf1061a315ec43e6c652cd9e44e Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Fri, 15 Aug 2025 15:38:47 -0500 Subject: [PATCH 07/31] update Urianger Augurelt to support full cost reduction --- .../src/mage/cards/u/UriangerAugurelt.java | 22 +++--- .../single/fic/UriangerAugureltTest.java | 75 +++++++++++++++++++ 2 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/fic/UriangerAugureltTest.java diff --git a/Mage.Sets/src/mage/cards/u/UriangerAugurelt.java b/Mage.Sets/src/mage/cards/u/UriangerAugurelt.java index df9fcd03c86..b30eaba92de 100644 --- a/Mage.Sets/src/mage/cards/u/UriangerAugurelt.java +++ b/Mage.Sets/src/mage/cards/u/UriangerAugurelt.java @@ -6,6 +6,9 @@ import mage.abilities.Ability; import mage.abilities.common.PlayLandOrCastSpellTriggeredAbility; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.GainLifeEffect; @@ -14,7 +17,6 @@ import mage.cards.Card; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.*; -import mage.game.Controllable; import mage.game.Game; import mage.players.Player; import mage.target.targetpointer.FixedTarget; @@ -142,14 +144,16 @@ class UriangerAugureltPlayEffect extends AsThoughEffectImpl { if (card.isLand(game)) { return true; } - // TODO: This should ideally apply the reduction while the spell is being cast because effects that increase the cost apply first - Optional.ofNullable(source) - .map(Controllable::getControllerId) - .map(game::getPlayer) - .ifPresent(player -> player.setCastSourceIdWithAlternateMana( - card.getId(), CardUtil.reduceCost(card.getManaCost(), 2), - null, MageIdentifier.UriangerAugureltAlternateCast - )); + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + ManaCosts newCost = CardUtil.reduceCost(card.getManaCost(), 2); + if (newCost.isEmpty()) { + newCost.add(new GenericManaCost(0)); + } + controller.setCastSourceIdWithAlternateMana( + card.getId(), newCost, null, MageIdentifier.UriangerAugureltAlternateCast + ); + } return true; } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/fic/UriangerAugureltTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/fic/UriangerAugureltTest.java new file mode 100644 index 00000000000..cf66bcd37fd --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/fic/UriangerAugureltTest.java @@ -0,0 +1,75 @@ +package org.mage.test.cards.single.fic; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.UntapAllControllerEffect; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +public class UriangerAugureltTest extends CardTestPlayerBase { + + private static final String urianger = "Urianger Augurelt"; + private static final String arcaneSignet = "Arcane Signet"; + private static final String howlingMine = "Howling Mine"; + private static final String thoughtVessel = "Thought Vessel"; + private static final String benalishKnight = "Benalish Knight"; + + @Test + public void uriangerAugureltTest() { + setStrictChooseMode(true); + skipInitShuffling(); + removeAllCardsFromLibrary(playerA); + + Ability ability = new SimpleActivatedAbility( + Zone.ALL, + new UntapAllControllerEffect(StaticFilters.FILTER_CONTROLLED_A_CREATURE), + new ManaCostsImpl<>("") + ); + addCustomCardWithAbility("Untap creatures", playerA, ability); + + addCard(Zone.BATTLEFIELD, playerA, urianger); + addCard(Zone.LIBRARY, playerA, "Plains", 3); + addCard(Zone.LIBRARY, playerA, arcaneSignet); + addCard(Zone.LIBRARY, playerA, howlingMine); + addCard(Zone.LIBRARY, playerA, thoughtVessel); + addCard(Zone.LIBRARY, playerA, benalishKnight); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Draw Arcanum"); + setChoice(playerA, true, 4); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "untap all"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Draw Arcanum"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "untap all"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Draw Arcanum"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "untap all"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Draw Arcanum"); + + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Arcanum"); + waitStackResolved(3, PhaseStep.PRECOMBAT_MAIN); + playLand(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Plains"); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, benalishKnight, true); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, arcaneSignet, true); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, howlingMine, true); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, thoughtVessel, true); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, benalishKnight, 1); + assertPermanentCount(playerA, arcaneSignet, 1); + assertPermanentCount(playerA, howlingMine, 1); + assertPermanentCount(playerA, thoughtVessel, 1); + assertLife(playerA, 20 + 2 * 4); + } +} From 6008dc279e1f2cfaf133a32ac806e7b37419e62e Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 15 Aug 2025 16:49:18 -0400 Subject: [PATCH 08/31] [TLA] Implement Aang's Journey --- Mage.Sets/src/mage/cards/a/AangsJourney.java | 64 +++++++++++++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 1 + .../common/TargetCardAndOrCardInLibrary.java | 16 ++--- 3 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/a/AangsJourney.java diff --git a/Mage.Sets/src/mage/cards/a/AangsJourney.java b/Mage.Sets/src/mage/cards/a/AangsJourney.java new file mode 100644 index 00000000000..c43bd803446 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AangsJourney.java @@ -0,0 +1,64 @@ +package mage.cards.a; + +import mage.abilities.condition.common.KickedCondition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; +import mage.abilities.keyword.KickerAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.StaticFilters; +import mage.filter.predicate.Predicate; +import mage.filter.predicate.Predicates; +import mage.target.common.TargetCardAndOrCardInLibrary; +import mage.target.common.TargetCardInLibrary; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class AangsJourney extends CardImpl { + + private static final Predicate predicate = Predicates.and( + SuperType.BASIC.getPredicate(), + CardType.LAND.getPredicate() + ); + + public AangsJourney(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}"); + + this.subtype.add(SubType.LESSON); + + // Kicker {2} + this.addAbility(new KickerAbility("{2}")); + + // Search your library for a basic land card. If this spell was kicked, instead search your library for a basic land card and a Shrine card. Reveal those cards, put them into your hand, then shuffle. + this.getSpellAbility().addEffect(new ConditionalOneShotEffect( + new SearchLibraryPutInHandEffect(new TargetCardAndOrCardInLibrary( + predicate, SubType.SHRINE.getPredicate(), + "a basic land card and/or a Shrine card" + ), true), + new SearchLibraryPutInHandEffect(new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND), true), + KickedCondition.ONCE, "search your library for a basic land card. If this spell was kicked, " + + "instead search your library for a basic land card and a Shrine card. " + + "Reveal those cards, put them into your hand, then shuffle" + )); + + // You gain 2 life. + this.getSpellAbility().addEffect(new GainLifeEffect(2).concatBy("
")); + } + + private AangsJourney(final AangsJourney card) { + super(card); + } + + @Override + public AangsJourney copy() { + return new AangsJourney(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 4c3fd56748c..9e773993bea 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -27,6 +27,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Aang's Iceberg", 336, Rarity.RARE, mage.cards.a.AangsIceberg.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Aang's Iceberg", 5, Rarity.RARE, mage.cards.a.AangsIceberg.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Aang's Journey", 1, Rarity.COMMON, mage.cards.a.AangsJourney.class)); cards.add(new SetCardInfo("Aang, Master of Elements", 207, Rarity.MYTHIC, mage.cards.a.AangMasterOfElements.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Aang, Master of Elements", 363, Rarity.MYTHIC, mage.cards.a.AangMasterOfElements.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Aang, the Last Airbender", 4, Rarity.UNCOMMON, mage.cards.a.AangTheLastAirbender.class)); diff --git a/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInLibrary.java b/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInLibrary.java index 3b5b3461715..3def0cb8984 100644 --- a/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInLibrary.java +++ b/Mage/src/main/java/mage/target/common/TargetCardAndOrCardInLibrary.java @@ -38,14 +38,6 @@ public class TargetCardAndOrCardInLibrary extends TargetCardInLibrary { private final PredicateCardAssignment assignment; - /** - * a [firstType] card and/or a [secondType] card - */ - protected TargetCardAndOrCardInLibrary(Predicate firstPredicate, Predicate secondPredicate, String filterText) { - super(0, 2, makeFilter(firstPredicate, secondPredicate, filterText)); - this.assignment = new PredicateCardAssignment(firstPredicate, secondPredicate); - } - public TargetCardAndOrCardInLibrary(CardType firstType, CardType secondType) { this(firstType.getPredicate(), secondType.getPredicate(), makeFilterText( CardUtil.getTextWithFirstCharLowerCase(firstType.toString()), @@ -60,6 +52,14 @@ public class TargetCardAndOrCardInLibrary extends TargetCardInLibrary { this(firstType.getPredicate(), secondType.getPredicate(), makeFilterText(firstType.getDescription(), secondType.getDescription())); } + /** + * a [firstType] card and/or a [secondType] card + */ + public TargetCardAndOrCardInLibrary(Predicate firstPredicate, Predicate secondPredicate, String filterText) { + super(0, 2, makeFilter(firstPredicate, secondPredicate, filterText)); + this.assignment = new PredicateCardAssignment(firstPredicate, secondPredicate); + } + protected TargetCardAndOrCardInLibrary(final TargetCardAndOrCardInLibrary target) { super(target); this.assignment = target.assignment; From 168e2a27ec3c631bda5fa414d235fe6950b2b808 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Fri, 15 Aug 2025 16:58:14 -0400 Subject: [PATCH 09/31] fix verify failure --- Mage.Sets/src/mage/cards/h/HaruHiddenTalent.java | 3 +++ .../src/test/java/mage/verify/VerifyCardDataTest.java | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/h/HaruHiddenTalent.java b/Mage.Sets/src/mage/cards/h/HaruHiddenTalent.java index a7a1f7f82a5..bad79858e09 100644 --- a/Mage.Sets/src/mage/cards/h/HaruHiddenTalent.java +++ b/Mage.Sets/src/mage/cards/h/HaruHiddenTalent.java @@ -9,6 +9,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; +import mage.constants.SuperType; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledPermanent; import mage.filter.predicate.mageobject.AnotherPredicate; @@ -29,6 +30,8 @@ public final class HaruHiddenTalent extends CardImpl { public HaruHiddenTalent(UUID ownerId, CardSetInfo setInfo) { super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{G}"); + + this.supertype.add(SuperType.LEGENDARY); this.subtype.add(SubType.HUMAN); this.subtype.add(SubType.PEASANT); this.subtype.add(SubType.ALLY); diff --git a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java index d205547d0a7..ebebe6c5903 100644 --- a/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java +++ b/Mage.Verify/src/test/java/mage/verify/VerifyCardDataTest.java @@ -2079,6 +2079,7 @@ public class VerifyCardDataTest { } } } + private void checkSubtypes(Card card, MtgJsonCard ref) { if (skipListHaveName(SKIP_LIST_SUBTYPE, card.getExpansionSetCode(), card.getName())) { return; @@ -2343,7 +2344,11 @@ public class VerifyCardDataTest { // search and check dies related abilities - String rules = triggeredAbility.getRule(); + // remove reminder text + String rules = triggeredAbility + .getRule() + .replaceAll("(?i) \\(.+\\)", "") + .replaceAll("(?i) \\(.+\\)", ""); if (ignoredAbilities.stream().anyMatch(rules::contains)) { continue; } From faedb81a5cf8c3ed7d0e4eb4e14c42faeebae6d4 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Fri, 15 Aug 2025 20:47:10 -0400 Subject: [PATCH 10/31] [PIP] Rex doesn't restrict stolen abilities to only once per turn. --- Mage.Sets/src/mage/cards/r/RexCyberHound.java | 1 - 1 file changed, 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/r/RexCyberHound.java b/Mage.Sets/src/mage/cards/r/RexCyberHound.java index 3b04fb97384..90f7f61121b 100644 --- a/Mage.Sets/src/mage/cards/r/RexCyberHound.java +++ b/Mage.Sets/src/mage/cards/r/RexCyberHound.java @@ -129,7 +129,6 @@ class RexCyberhoundContinuousEffect extends ContinuousEffectImpl { for (Ability ability : card.getAbilities(game)) { if (ability.isActivatedAbility()) { ActivatedAbility copyAbility = (ActivatedAbility) ability.copy(); - copyAbility.setMaxActivationsPerTurn(1); perm.addAbility(copyAbility, source.getSourceId(), game, true); } } From b3c0090fba999709c7d5488df84923a05b3bfbcc Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 16 Aug 2025 10:17:19 -0400 Subject: [PATCH 11/31] [TLE] update spoiler --- .../sets/AvatarTheLastAirbenderEternal.java | 84 ++++++++++++++----- Utils/mtg-cards-data.txt | 42 +++++++++- 2 files changed, 103 insertions(+), 23 deletions(-) diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java index 877f0be8ad6..a9db35f157d 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java @@ -26,49 +26,83 @@ public final class AvatarTheLastAirbenderEternal extends ExpansionSet { this.rotationSet = true; this.hasBasicLands = false; - cards.add(new SetCardInfo("Aang's Defense", 211, Rarity.COMMON, mage.cards.a.AangsDefense.class)); - cards.add(new SetCardInfo("Aang, Air Nomad", 210, Rarity.RARE, mage.cards.a.AangAirNomad.class)); + cards.add(new SetCardInfo("Aang's Defense", 211, Rarity.COMMON, mage.cards.a.AangsDefense.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Aang's Defense", 266, Rarity.COMMON, mage.cards.a.AangsDefense.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Aang, Air Nomad", 210, Rarity.RARE, mage.cards.a.AangAirNomad.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Aang, Air Nomad", 265, Rarity.RARE, mage.cards.a.AangAirNomad.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Aang, Airbending Master", 74, Rarity.MYTHIC, mage.cards.a.AangAirbendingMaster.class)); - cards.add(new SetCardInfo("Aardvark Sloth", 212, Rarity.COMMON, mage.cards.a.AardvarkSloth.class)); + cards.add(new SetCardInfo("Aardvark Sloth", 212, Rarity.COMMON, mage.cards.a.AardvarkSloth.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Aardvark Sloth", 267, Rarity.COMMON, mage.cards.a.AardvarkSloth.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Allied Teamwork", 213, Rarity.RARE, mage.cards.a.AlliedTeamwork.class)); - cards.add(new SetCardInfo("Appa, Aang's Companion", 214, Rarity.UNCOMMON, mage.cards.a.AppaAangsCompanion.class)); + cards.add(new SetCardInfo("Appa, Aang's Companion", 214, Rarity.UNCOMMON, mage.cards.a.AppaAangsCompanion.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Appa, Aang's Companion", 268, Rarity.UNCOMMON, mage.cards.a.AppaAangsCompanion.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Bumi, Eclectic Earthbender", 248, Rarity.RARE, mage.cards.b.BumiEclecticEarthbender.class)); - cards.add(new SetCardInfo("Capital Guard", 234, Rarity.COMMON, mage.cards.c.CapitalGuard.class)); + cards.add(new SetCardInfo("Capital Guard", 234, Rarity.COMMON, mage.cards.c.CapitalGuard.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Capital Guard", 277, Rarity.COMMON, mage.cards.c.CapitalGuard.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Deny Entry", 222, Rarity.COMMON, mage.cards.d.DenyEntry.class)); - cards.add(new SetCardInfo("Dragon Moose", 235, Rarity.COMMON, mage.cards.d.DragonMoose.class)); + cards.add(new SetCardInfo("Dragon Moose", 235, Rarity.COMMON, mage.cards.d.DragonMoose.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Dragon Moose", 278, Rarity.COMMON, mage.cards.d.DragonMoose.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Earthbending Student", 249, Rarity.UNCOMMON, mage.cards.e.EarthbendingStudent.class)); cards.add(new SetCardInfo("Eel-Hounds", 250, Rarity.UNCOMMON, mage.cards.e.EelHounds.class)); cards.add(new SetCardInfo("Elephant-Rat", 228, Rarity.COMMON, mage.cards.e.ElephantRat.class)); cards.add(new SetCardInfo("Explore", 259, Rarity.COMMON, mage.cards.e.Explore.class)); - cards.add(new SetCardInfo("Explosive Shot", 236, Rarity.COMMON, mage.cards.e.ExplosiveShot.class)); + cards.add(new SetCardInfo("Explosive Shot", 236, Rarity.COMMON, mage.cards.e.ExplosiveShot.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Explosive Shot", 279, Rarity.COMMON, mage.cards.e.ExplosiveShot.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Feed the Swarm", 257, Rarity.COMMON, mage.cards.f.FeedTheSwarm.class)); cards.add(new SetCardInfo("Fire Nation Ambushers", 229, Rarity.COMMON, mage.cards.f.FireNationAmbushers.class)); cards.add(new SetCardInfo("Fire Nation Archers", 237, Rarity.RARE, mage.cards.f.FireNationArchers.class)); cards.add(new SetCardInfo("Fire Nation Sentinels", 230, Rarity.RARE, mage.cards.f.FireNationSentinels.class)); - cards.add(new SetCardInfo("Fire Nation Soldier", 238, Rarity.COMMON, mage.cards.f.FireNationSoldier.class)); - cards.add(new SetCardInfo("Fire Nation's Conquest", 239, Rarity.UNCOMMON, mage.cards.f.FireNationsConquest.class)); + cards.add(new SetCardInfo("Fire Nation Soldier", 238, Rarity.COMMON, mage.cards.f.FireNationSoldier.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Fire Nation Soldier", 280, Rarity.COMMON, mage.cards.f.FireNationSoldier.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Fire Nation's Conquest", 239, Rarity.UNCOMMON, mage.cards.f.FireNationsConquest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Fire Nation's Conquest", 281, Rarity.UNCOMMON, mage.cards.f.FireNationsConquest.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Flying Dolphin-Fish", 223, Rarity.COMMON, mage.cards.f.FlyingDolphinFish.class)); cards.add(new SetCardInfo("Force of Negation", 13, Rarity.MYTHIC, mage.cards.f.ForceOfNegation.class)); cards.add(new SetCardInfo("Frog-Squirrels", 251, Rarity.COMMON, mage.cards.f.FrogSquirrels.class)); cards.add(new SetCardInfo("Gilacorn", 231, Rarity.COMMON, mage.cards.g.Gilacorn.class)); cards.add(new SetCardInfo("Hippo-Cows", 252, Rarity.COMMON, mage.cards.h.HippoCows.class)); - cards.add(new SetCardInfo("Iroh, Firebending Instructor", 240, Rarity.UNCOMMON, mage.cards.i.IrohFirebendingInstructor.class)); - cards.add(new SetCardInfo("Katara, Heroic Healer", 215, Rarity.UNCOMMON, mage.cards.k.KataraHeroicHealer.class)); + cards.add(new SetCardInfo("Iroh, Firebending Instructor", 240, Rarity.UNCOMMON, mage.cards.i.IrohFirebendingInstructor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Iroh, Firebending Instructor", 282, Rarity.UNCOMMON, mage.cards.i.IrohFirebendingInstructor.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Katara, Heroic Healer", 215, Rarity.UNCOMMON, mage.cards.k.KataraHeroicHealer.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Katara, Heroic Healer", 269, Rarity.UNCOMMON, mage.cards.k.KataraHeroicHealer.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Katara, Waterbending Master", 93, Rarity.MYTHIC, mage.cards.k.KataraWaterbendingMaster.class)); - cards.add(new SetCardInfo("Komodo Rhino", 241, Rarity.COMMON, mage.cards.k.KomodoRhino.class)); + cards.add(new SetCardInfo("Komodo Rhino", 241, Rarity.COMMON, mage.cards.k.KomodoRhino.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Komodo Rhino", 283, Rarity.COMMON, mage.cards.k.KomodoRhino.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Kyoshi Warrior Guard", 216, Rarity.COMMON, mage.cards.k.KyoshiWarriorGuard.class)); cards.add(new SetCardInfo("Lion Vulture", 232, Rarity.RARE, mage.cards.l.LionVulture.class)); cards.add(new SetCardInfo("Lost in the Spirit World", 224, Rarity.UNCOMMON, mage.cards.l.LostInTheSpiritWorld.class)); cards.add(new SetCardInfo("Loyal Fire Sage", 242, Rarity.UNCOMMON, mage.cards.l.LoyalFireSage.class)); cards.add(new SetCardInfo("Match the Odds", 253, Rarity.UNCOMMON, mage.cards.m.MatchTheOdds.class)); cards.add(new SetCardInfo("Mechanical Glider", 256, Rarity.COMMON, mage.cards.m.MechanicalGlider.class)); - cards.add(new SetCardInfo("Momo, Rambunctious Rascal", 217, Rarity.UNCOMMON, mage.cards.m.MomoRambunctiousRascal.class)); + cards.add(new SetCardInfo("Momo, Rambunctious Rascal", 217, Rarity.UNCOMMON, mage.cards.m.MomoRambunctiousRascal.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Momo, Rambunctious Rascal", 270, Rarity.UNCOMMON, mage.cards.m.MomoRambunctiousRascal.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 289, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 290, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 291, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 292, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 293, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 294, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 295, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Mountain", 296, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Path to Redemption", 271, Rarity.COMMON, mage.cards.p.PathToRedemption.class)); + cards.add(new SetCardInfo("Plains", 297, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 298, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 299, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 300, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 301, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 302, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 303, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Plains", 304, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Purple Pentapus", 233, Rarity.COMMON, mage.cards.p.PurplePentapus.class)); + cards.add(new SetCardInfo("Razor Rings", 272, Rarity.COMMON, mage.cards.r.RazorRings.class)); cards.add(new SetCardInfo("Roku's Mastery", 243, Rarity.UNCOMMON, mage.cards.r.RokusMastery.class)); - cards.add(new SetCardInfo("Run Amok", 258, Rarity.COMMON, mage.cards.r.RunAmok.class)); + cards.add(new SetCardInfo("Run Amok", 258, Rarity.COMMON, mage.cards.r.RunAmok.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Run Amok", 284, Rarity.COMMON, mage.cards.r.RunAmok.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Seismic Tutelage", 254, Rarity.RARE, mage.cards.s.SeismicTutelage.class)); - cards.add(new SetCardInfo("Sledding Otter-Penguin", 218, Rarity.COMMON, mage.cards.s.SleddingOtterPenguin.class)); - cards.add(new SetCardInfo("Sokka, Wolf Cove's Protector", 219, Rarity.UNCOMMON, mage.cards.s.SokkaWolfCovesProtector.class)); + cards.add(new SetCardInfo("Sledding Otter-Penguin", 218, Rarity.COMMON, mage.cards.s.SleddingOtterPenguin.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sledding Otter-Penguin", 273, Rarity.COMMON, mage.cards.s.SleddingOtterPenguin.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sokka, Wolf Cove's Protector", 219, Rarity.UNCOMMON, mage.cards.s.SokkaWolfCovesProtector.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Sokka, Wolf Cove's Protector", 274, Rarity.UNCOMMON, mage.cards.s.SokkaWolfCovesProtector.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("The Cabbage Merchant", 134, Rarity.RARE, mage.cards.t.TheCabbageMerchant.class)); cards.add(new SetCardInfo("The Great Henge", 41, Rarity.MYTHIC, mage.cards.t.TheGreatHenge.class)); cards.add(new SetCardInfo("The Terror of Serpent's Pass", 225, Rarity.RARE, mage.cards.t.TheTerrorOfSerpentsPass.class)); @@ -77,14 +111,20 @@ public final class AvatarTheLastAirbenderEternal extends ExpansionSet { cards.add(new SetCardInfo("Thriving Heath", 262, Rarity.COMMON, mage.cards.t.ThrivingHeath.class)); cards.add(new SetCardInfo("Thriving Isle", 263, Rarity.COMMON, mage.cards.t.ThrivingIsle.class)); cards.add(new SetCardInfo("Thriving Moor", 264, Rarity.COMMON, mage.cards.t.ThrivingMoor.class)); - cards.add(new SetCardInfo("Tundra Wall", 220, Rarity.COMMON, mage.cards.t.TundraWall.class)); + cards.add(new SetCardInfo("Tundra Wall", 220, Rarity.COMMON, mage.cards.t.TundraWall.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Tundra Wall", 275, Rarity.COMMON, mage.cards.t.TundraWall.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Turtle-Seals", 226, Rarity.COMMON, mage.cards.t.TurtleSeals.class)); - cards.add(new SetCardInfo("Warship Scout", 244, Rarity.COMMON, mage.cards.w.WarshipScout.class)); + cards.add(new SetCardInfo("Warship Scout", 244, Rarity.COMMON, mage.cards.w.WarshipScout.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Warship Scout", 285, Rarity.COMMON, mage.cards.w.WarshipScout.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Water Whip", 227, Rarity.RARE, mage.cards.w.WaterWhip.class)); - cards.add(new SetCardInfo("Wolf Cove Villager", 221, Rarity.COMMON, mage.cards.w.WolfCoveVillager.class)); - cards.add(new SetCardInfo("Zhao, the Seething Flame", 245, Rarity.UNCOMMON, mage.cards.z.ZhaoTheSeethingFlame.class)); - cards.add(new SetCardInfo("Zuko's Offense", 247, Rarity.COMMON, mage.cards.z.ZukosOffense.class)); - cards.add(new SetCardInfo("Zuko, Avatar Hunter", 246, Rarity.RARE, mage.cards.z.ZukoAvatarHunter.class)); + cards.add(new SetCardInfo("Wolf Cove Villager", 221, Rarity.COMMON, mage.cards.w.WolfCoveVillager.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Wolf Cove Villager", 276, Rarity.COMMON, mage.cards.w.WolfCoveVillager.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Zhao, the Seething Flame", 245, Rarity.UNCOMMON, mage.cards.z.ZhaoTheSeethingFlame.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Zhao, the Seething Flame", 286, Rarity.UNCOMMON, mage.cards.z.ZhaoTheSeethingFlame.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Zuko's Offense", 247, Rarity.COMMON, mage.cards.z.ZukosOffense.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Zuko's Offense", 288, Rarity.COMMON, mage.cards.z.ZukosOffense.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Zuko, Avatar Hunter", 246, Rarity.RARE, mage.cards.z.ZukoAvatarHunter.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Zuko, Avatar Hunter", 287, Rarity.RARE, mage.cards.z.ZukoAvatarHunter.class, NON_FULL_USE_VARIOUS)); cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); } diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 9c8920538a8..aa3680dc96b 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -59940,7 +59940,7 @@ Aang, Air Nomad|Avatar: The Last Airbender Eternal|210|R|{3}{W}{W}|Legendary Cre Aang's Defense|Avatar: The Last Airbender Eternal|211|C|{W}|Instant|||Target blocking creature you control gets +2/+2 until end of turn.$Draw a card.| Aardvark Sloth|Avatar: The Last Airbender Eternal|212|C|{3}{W}|Creature - Sloth Beast|3|3|Lifelink| Allied Teamwork|Avatar: The Last Airbender Eternal|213|R|{2}{W}|Enchantment|||When this enchantment enters, create a 1/1 white Ally creature token.$Allies you control get +1/+1.| -Appa, Aang's Companion|Avatar: The Last Airbender Eternal|214|U|{3}{W}|Legendary Creature - Bison Ally|2|4|Flying$Whenever Appa attacks, another target attacking creature without flying gains flying until until end of turn.| +Appa, Aang's Companion|Avatar: The Last Airbender Eternal|214|U|{3}{W}|Legendary Creature - Bison Ally|2|4|Flying$Whenever Appa attacks, another target attacking creature without flying gains flying until end of turn.| Katara, Heroic Healer|Avatar: The Last Airbender Eternal|215|U|{4}{W}|Legendary Creature - Human Warrior Ally|2|3|Lifelink$When Katara enters, put a +1/+1 counter on each other creature you control.| Kyoshi Warrior Guard|Avatar: The Last Airbender Eternal|216|C|{1}{W}|Creature - Human Warrior Ally|2|3|| Momo, Rambunctious Rascal|Avatar: The Last Airbender Eternal|217|U|{2}{W}|Legendary Creature - Lemur Bat Ally|1|1|Flying$When Momo enters, he deals 4 damage to target tapped creature an opponent controls.| @@ -59991,3 +59991,43 @@ Thriving Grove|Avatar: The Last Airbender Eternal|261|C||Land|||This land enters Thriving Heath|Avatar: The Last Airbender Eternal|262|C||Land|||This land enters tapped. As it enters, choose a color other than white.${T}: Add {W} or one mana of the chosen color.| Thriving Isle|Avatar: The Last Airbender Eternal|263|C||Land|||This land enters tapped. As it enters, choose a color other than blue.${T}: Add {U} or one mana of the chosen color.| Thriving Moor|Avatar: The Last Airbender Eternal|264|C||Land|||This land enters tapped. As it enters, choose a color other than black.${T}: Add {B} or one mana of the chosen color.| +Aang, Air Nomad|Avatar: The Last Airbender Eternal|265|R|{3}{W}{W}|Legendary Creature - Human Avatar Ally|5|4|Flying$Vigilance$Other creatures you control have vigilance.| +Aang's Defense|Avatar: The Last Airbender Eternal|266|C|{W}|Instant|||Target blocking creature you control gets +2/+2 until end of turn.$Draw a card.| +Aardvark Sloth|Avatar: The Last Airbender Eternal|267|C|{3}{W}|Creature - Sloth Beast|3|3|Lifelink| +Appa, Aang's Companion|Avatar: The Last Airbender Eternal|268|U|{3}{W}|Legendary Creature - Bison Ally|2|4|Flying$Whenever Appa attacks, another target attacking creature without flying gains flying until end of turn.| +Katara, Heroic Healer|Avatar: The Last Airbender Eternal|269|U|{4}{W}|Legendary Creature - Human Warrior Ally|2|3|Lifelink$When Katara enters, put a +1/+1 counter on each other creature you control.| +Momo, Rambunctious Rascal|Avatar: The Last Airbender Eternal|270|U|{2}{W}|Legendary Creature - Lemur Bat Ally|1|1|Flying$When Momo enters, he deals 4 damage to target tapped creature an opponent controls.| +Path to Redemption|Avatar: The Last Airbender Eternal|271|C|{1}{W}|Enchantment - Aura|||Enchant creature$Enchanted creature can't attack or block.${5}, Sacrifice this Aura: Exile enchanted creature. Create a 1/1 white Ally creature token. Activate only during your turn.| +Razor Rings|Avatar: The Last Airbender Eternal|272|C|{1}{W}|Instant|||Razor Rings deals 4 damage to target attacking or blocking creature. You gain life equal to the excess damage dealt this way.| +Sledding Otter-Penguin|Avatar: The Last Airbender Eternal|273|C|{2}{W}|Creature - Otter Bird|2|3|{3}: Put a +1/+1 counter on this creature.| +Sokka, Wolf Cove's Protector|Avatar: The Last Airbender Eternal|274|U|{2}{W}|Legendary Creature - Human Warrior Ally|3|3|Vigilance| +Tundra Wall|Avatar: The Last Airbender Eternal|275|C|{1}{W}|Creature - Wall|0|4|Defender| +Wolf Cove Villager|Avatar: The Last Airbender Eternal|276|C|{W}|Creature - Human Peasant|2|2|This creature enters tapped.| +Capital Guard|Avatar: The Last Airbender Eternal|277|C|{1}{R}|Creature - Human Soldier|2|2|| +Dragon Moose|Avatar: The Last Airbender Eternal|278|C|{3}{R}|Creature - Dragon Elk|3|3|Haste| +Explosive Shot|Avatar: The Last Airbender Eternal|279|C|{1}{R}|Sorcery|||Explosive Shot deals 4 damage to target creature.| +Fire Nation Soldier|Avatar: The Last Airbender Eternal|280|C|{2}{R}|Creature - Human Soldier|3|2|Haste| +Fire Nation's Conquest|Avatar: The Last Airbender Eternal|281|U|{2}{R}|Enchantment|||Creatures you control get +1/+0.| +Iroh, Firebending Instructor|Avatar: The Last Airbender Eternal|282|U|{2}{R}|Legendary Creature - Human Noble Ally|2|2|Whenever Iroh attacks, attacking creatures get +1/+1 until end of turn.| +Komodo Rhino|Avatar: The Last Airbender Eternal|283|C|{3}{R}|Creature - Lizard Rhino|5|2|Trample| +Run Amok|Avatar: The Last Airbender Eternal|284|C|{1}{R}|Instant|||Target attacking creature gets +3/+3 and gains trample until end of turn.| +Warship Scout|Avatar: The Last Airbender Eternal|285|C|{R}|Creature - Human Scout|2|1|| +Zhao, the Seething Flame|Avatar: The Last Airbender Eternal|286|U|{4}{R}|Legendary Creature - Human Soldier|5|5|Menace| +Zuko, Avatar Hunter|Avatar: The Last Airbender Eternal|287|R|{3}{R}{R}|Legendary Creature - Human Noble|4|5|Reach$Whenever you cast a red spell, create a 2/2 red Soldier creature token.| +Zuko's Offense|Avatar: The Last Airbender Eternal|288|C|{R}|Sorcery|||Zuko's Offense deals 2 damage to any target.| +Mountain|Avatar: The Last Airbender Eternal|289|C||Basic Land - Mountain|||({T}: Add {R}.)| +Mountain|Avatar: The Last Airbender Eternal|290|C||Basic Land - Mountain|||({T}: Add {R}.)| +Mountain|Avatar: The Last Airbender Eternal|291|C||Basic Land - Mountain|||({T}: Add {R}.)| +Mountain|Avatar: The Last Airbender Eternal|292|C||Basic Land - Mountain|||({T}: Add {R}.)| +Mountain|Avatar: The Last Airbender Eternal|293|C||Basic Land - Mountain|||({T}: Add {R}.)| +Mountain|Avatar: The Last Airbender Eternal|294|C||Basic Land - Mountain|||({T}: Add {R}.)| +Mountain|Avatar: The Last Airbender Eternal|295|C||Basic Land - Mountain|||({T}: Add {R}.)| +Mountain|Avatar: The Last Airbender Eternal|296|C||Basic Land - Mountain|||({T}: Add {R}.)| +Plains|Avatar: The Last Airbender Eternal|297|C||Basic Land - Plains|||({T}: Add {W}.)| +Plains|Avatar: The Last Airbender Eternal|298|C||Basic Land - Plains|||({T}: Add {W}.)| +Plains|Avatar: The Last Airbender Eternal|299|C||Basic Land - Plains|||({T}: Add {W}.)| +Plains|Avatar: The Last Airbender Eternal|300|C||Basic Land - Plains|||({T}: Add {W}.)| +Plains|Avatar: The Last Airbender Eternal|301|C||Basic Land - Plains|||({T}: Add {W}.)| +Plains|Avatar: The Last Airbender Eternal|302|C||Basic Land - Plains|||({T}: Add {W}.)| +Plains|Avatar: The Last Airbender Eternal|303|C||Basic Land - Plains|||({T}: Add {W}.)| +Plains|Avatar: The Last Airbender Eternal|304|C||Basic Land - Plains|||({T}: Add {W}.)| From 13d4ee1d656fa6db81184e60a8b3d8d747a9823b Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 16 Aug 2025 10:17:40 -0400 Subject: [PATCH 12/31] [TLA] update spoiler --- Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java | 5 +++++ Utils/mtg-cards-data.txt | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 9e773993bea..d7146afaf28 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -64,6 +64,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("First-Time Flyer", 49, Rarity.COMMON, mage.cards.f.FirstTimeFlyer.class)); cards.add(new SetCardInfo("Flexible Waterbender", 50, Rarity.COMMON, mage.cards.f.FlexibleWaterbender.class)); cards.add(new SetCardInfo("Flopsie, Bumi's Buddy", 179, Rarity.UNCOMMON, mage.cards.f.FlopsieBumisBuddy.class)); + cards.add(new SetCardInfo("Forest", 286, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Forest", 291, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Geyser Leaper", 52, Rarity.COMMON, mage.cards.g.GeyserLeaper.class)); cards.add(new SetCardInfo("Giant Koi", 53, Rarity.COMMON, mage.cards.g.GiantKoi.class)); @@ -74,6 +75,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Hog-Monkey", 104, Rarity.COMMON, mage.cards.h.HogMonkey.class)); cards.add(new SetCardInfo("How to Start a Riot", 140, Rarity.COMMON, mage.cards.h.HowToStartARiot.class)); cards.add(new SetCardInfo("Iguana Parrot", 56, Rarity.COMMON, mage.cards.i.IguanaParrot.class)); + cards.add(new SetCardInfo("Island", 283, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Island", 288, Rarity.LAND, mage.cards.basiclands.Island.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("It'll Quench Ya!", 58, Rarity.COMMON, mage.cards.i.ItllQuenchYa.class)); cards.add(new SetCardInfo("Jeong Jeong's Deserters", 25, Rarity.COMMON, mage.cards.j.JeongJeongsDeserters.class)); @@ -89,12 +91,14 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Master Piandao", 28, Rarity.UNCOMMON, mage.cards.m.MasterPiandao.class)); cards.add(new SetCardInfo("Merchant of Many Hats", 110, Rarity.COMMON, mage.cards.m.MerchantOfManyHats.class)); cards.add(new SetCardInfo("Mongoose Lizard", 148, Rarity.COMMON, mage.cards.m.MongooseLizard.class)); + cards.add(new SetCardInfo("Mountain", 285, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Mountain", 290, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Ostrich-Horse", 188, Rarity.COMMON, mage.cards.o.OstrichHorse.class)); cards.add(new SetCardInfo("Otter-Penguin", 67, Rarity.COMMON, mage.cards.o.OtterPenguin.class)); cards.add(new SetCardInfo("Ozai's Cruelty", 113, Rarity.UNCOMMON, mage.cards.o.OzaisCruelty.class)); cards.add(new SetCardInfo("Path to Redemption", 31, Rarity.COMMON, mage.cards.p.PathToRedemption.class)); cards.add(new SetCardInfo("Pillar Launch", 189, Rarity.COMMON, mage.cards.p.PillarLaunch.class)); + cards.add(new SetCardInfo("Plains", 282, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Plains", 287, Rarity.LAND, mage.cards.basiclands.Plains.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Pretending Poxbearers", 237, Rarity.COMMON, mage.cards.p.PretendingPoxbearers.class)); cards.add(new SetCardInfo("Rabaroo Troop", 32, Rarity.COMMON, mage.cards.r.RabarooTroop.class)); @@ -114,6 +118,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Sokka, Lateral Strategist", 241, Rarity.UNCOMMON, mage.cards.s.SokkaLateralStrategist.class)); cards.add(new SetCardInfo("Southern Air Temple", 36, Rarity.UNCOMMON, mage.cards.s.SouthernAirTemple.class)); cards.add(new SetCardInfo("Suki, Kyoshi Warrior", 243, Rarity.UNCOMMON, mage.cards.s.SukiKyoshiWarrior.class)); + cards.add(new SetCardInfo("Swamp", 284, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Swamp", 289, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Toph, the Blind Bandit", 198, Rarity.UNCOMMON, mage.cards.t.TophTheBlindBandit.class)); cards.add(new SetCardInfo("Toph, the First Metalbender", 247, Rarity.RARE, mage.cards.t.TophTheFirstMetalbender.class, NON_FULL_USE_VARIOUS)); diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index aa3680dc96b..e2c195fa7b8 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -59737,7 +59737,7 @@ Flexible Waterbender|Avatar: The Last Airbender|50|C|{3}{U}|Creature - Human War Geyser Leaper|Avatar: The Last Airbender|52|C|{4}{U}|Creature - Human Warrior Ally|4|3|Flying$Waterbend {4}: Draw a card, then discard a card.| Giant Koi|Avatar: The Last Airbender|53|C|{4}{U}{U}|Creature - Fish|5|7|Waterbend {3}: This creature can't be blocked this turn.$Islandcycling {2}| Iguana Parrot|Avatar: The Last Airbender|56|C|{2}{U}|Creature - Lizard Bird Pirate|2|2|Flying, vigilance$Prowess| -It'll Quench Ya!|Avatar: The Last Airbender|58|C|{1}{U}|Instant - Lesson|||Counter target spell unless its controller pays 2.| +It'll Quench Ya!|Avatar: The Last Airbender|58|C|{1}{U}|Instant - Lesson|||Counter target spell unless its controller pays {2}.| Katara, Bending Prodigy|Avatar: The Last Airbender|59|U|{2}{U}|Legendary Creature - Human Warrior Ally|2|3|At the beginning of your end step, if Katara is tapped, put a +1/+1 counter on her.$Waterbend {6}: Draw a card.| Master Pakku|Avatar: The Last Airbender|63|U|{1}{U}|Legendary Creature - Human Advisor Ally|1|3|Prowess$Whenever Master Pakku becomes tapped, target player mills X cards, where X is the number of Lesson cards in your graveyard.| Otter-Penguin|Avatar: The Last Airbender|67|C|{1}{U}|Creature - Otter Bird|2|1|Whenever you draw your second card each turn, this creature gets +1/+2 until end of turn and can't be blocked this turn.| @@ -59804,6 +59804,11 @@ Toph, the First Metalbender|Avatar: The Last Airbender|247|R|{1}{R}{G}{W}|Legend Vindictive Warden|Avatar: The Last Airbender|249|C|{2}{B/R}|Creature - Human Soldier|2|3|Menace$Firebending 1${3}: This creature deals 1 damage to each opponent.| Barrels of Blasting Jelly|Avatar: The Last Airbender|254|C|{1}|Artifact|||{1}: Add one mana of any color. Activate only once each turn.${5}, {T}, Sacrifice this artifact: It deals 5 damage to target creature.| Bender's Waterskin|Avatar: The Last Airbender|255|C|{3}|Artifact|||Untap this artifact during each other player's untap step.${T}: Add one mana of any color.| +Plains|Avatar: The Last Airbender|282|C||Basic Land - Plains|||({T}: Add {W}.)| +Island|Avatar: The Last Airbender|283|C||Basic Land - Island|||({T}: Add {U}.)| +Swamp|Avatar: The Last Airbender|284|C||Basic Land - Swamp|||({T}: Add {B}.)| +Mountain|Avatar: The Last Airbender|285|C||Basic Land - Mountain|||({T}: Add {R}.)| +Forest|Avatar: The Last Airbender|286|C||Basic Land - Forest|||({T}: Add {G}.)| Plains|Avatar: The Last Airbender|287|C||Basic Land - Plains|||({T}: Add {W}.)| Island|Avatar: The Last Airbender|288|C||Basic Land - Island|||({T}: Add {U}.)| Swamp|Avatar: The Last Airbender|289|C||Basic Land - Swamp|||({T}: Add {B}.)| @@ -59825,6 +59830,7 @@ Katara, the Fearless|Avatar: The Last Airbender|361|R|{G}{W}{U}|Legendary Creatu Toph, the First Metalbender|Avatar: The Last Airbender|362|R|{1}{R}{G}{W}|Legendary Creature - Human Warrior Ally|3|3|Nontoken artifacts you control are lands in addition to their other types.$At the beginning of your end step, earthbend 2.| Avatar Aang|Avatar: The Last Airbender|363|M|{R}{G}{W}{U}|Legendary Creature - Human Avatar Ally|4|4|Flying, firebending 2$Whenever you waterbend, earthbend, firebend, or airbend, draw a card. Then if you've done all four this turn, transform Avatar Aang.| Aang, Master of Elements|Avatar: The Last Airbender|363|M||Legendary Creature - Avatar Ally|6|6|Flying$Spells you cast cost {W}{U}{B}{R}{G} less to cast.$At the beginning of each upkeep, you may transform Aang, Master of Elements. If you do, you gain 4 life, draw four cards, put four +1/+1 counters on him, and he deals 4 damage to each opponent.| +Hakoda, Selfless Commander|Avatar: The Last Airbender|366|R|{3}{W}|Legendary Creature - Human Warrior Ally|3|5|Vigilance$You may look at the top card of your library any time.$You may cast Ally spells from the top of your library.$Sacrifice Hakoda: Creatures you control get +0/+5 and gain indestructible until end of turn.| Sokka, Bold Boomeranger|Avatar: The Last Airbender|383|R|{U}{R}|Legendary Creature - Human Warrior Ally|1|1|When Sokka enters, discard up to two cards, then draw that many cards.$Whenever you cast an artifact or Lesson spell, put a +1/+1 counter on Sokka.| Anti-Venom, Horrifying Healer|Marvel's Spider-Man|1|M|{W}{W}{W}{W}{W}|Legendary Creature - Symbiote Hero|5|5|When Anti-Venom enters, if he was cast, return target creature card from your graveyard to the battlefield.$If damage would be dealt to Anti-Venom, prevent that damage and put that many +1/+1 counters on him.| Aunt May|Marvel's Spider-Man|3|U|{W}|Legendary Creature - Human Citizen|0|2|Whenever another creature you control enters, you gain 1 life. If it's a Spider, put a +1/+1 counter on it.| From f0572e6099d75052f29d6d6eaafa7fdb21eb8ba4 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 16 Aug 2025 10:21:08 -0400 Subject: [PATCH 13/31] [TLA] Implement Hakoda, Selfless Commander --- .../mage/cards/h/HakodaSelflessCommander.java | 72 +++++++++++++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 1 + 2 files changed, 73 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/h/HakodaSelflessCommander.java diff --git a/Mage.Sets/src/mage/cards/h/HakodaSelflessCommander.java b/Mage.Sets/src/mage/cards/h/HakodaSelflessCommander.java new file mode 100644 index 00000000000..09523f531aa --- /dev/null +++ b/Mage.Sets/src/mage/cards/h/HakodaSelflessCommander.java @@ -0,0 +1,72 @@ +package mage.cards.h; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.SacrificeSourceCost; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityAllEffect; +import mage.abilities.effects.common.continuous.LookAtTopCardOfLibraryAnyTimeEffect; +import mage.abilities.effects.common.continuous.PlayFromTopOfLibraryEffect; +import mage.abilities.keyword.IndestructibleAbility; +import mage.abilities.keyword.VigilanceAbility; +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.StaticFilters; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class HakodaSelflessCommander extends CardImpl { + + private static final FilterCard filter = new FilterCard(SubType.ALLY, "Ally spells"); + + public HakodaSelflessCommander(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.WARRIOR); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(3); + this.toughness = new MageInt(5); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // You may look at the top card of your library any time. + this.addAbility(new SimpleStaticAbility(new LookAtTopCardOfLibraryAnyTimeEffect())); + + // You may cast Ally spells from the top of your library. + this.addAbility(new SimpleStaticAbility(new PlayFromTopOfLibraryEffect(filter))); + + // Sacrifice Hakoda: Creatures you control get +0/+5 and gain indestructible until end of turn. + Ability ability = new SimpleActivatedAbility( + new BoostControlledEffect(0, 5, Duration.EndOfTurn) + .setText("creatures you control get +0/+5"), + new SacrificeSourceCost() + ); + ability.addEffect(new GainAbilityAllEffect( + IndestructibleAbility.getInstance(), Duration.EndOfTurn, + StaticFilters.FILTER_CONTROLLED_CREATURE + ).setText("and gain indestructible until end of turn")); + this.addAbility(ability); + } + + private HakodaSelflessCommander(final HakodaSelflessCommander card) { + super(card); + } + + @Override + public HakodaSelflessCommander copy() { + return new HakodaSelflessCommander(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index d7146afaf28..b1f6b1977e1 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -69,6 +69,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Geyser Leaper", 52, Rarity.COMMON, mage.cards.g.GeyserLeaper.class)); cards.add(new SetCardInfo("Giant Koi", 53, Rarity.COMMON, mage.cards.g.GiantKoi.class)); cards.add(new SetCardInfo("Glider Kids", 21, Rarity.COMMON, mage.cards.g.GliderKids.class)); + cards.add(new SetCardInfo("Hakoda, Selfless Commander", 366, Rarity.RARE, mage.cards.h.HakodaSelflessCommander.class)); cards.add(new SetCardInfo("Haru, Hidden Talent", 182, Rarity.UNCOMMON, mage.cards.h.HaruHiddenTalent.class)); cards.add(new SetCardInfo("Heartless Act", 103, Rarity.UNCOMMON, mage.cards.h.HeartlessAct.class)); cards.add(new SetCardInfo("Hei Bai, Spirit of Balance", 225, Rarity.UNCOMMON, mage.cards.h.HeiBaiSpiritOfBalance.class)); From d38d0d871917f742341058305f518363d8610699 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 16 Aug 2025 10:32:12 -0400 Subject: [PATCH 14/31] [TLA] Implement Momo, Friendly Flier --- .../src/mage/cards/m/MomoFriendlyFlier.java | 132 ++++++++++++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 2 + 2 files changed, 134 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/m/MomoFriendlyFlier.java diff --git a/Mage.Sets/src/mage/cards/m/MomoFriendlyFlier.java b/Mage.Sets/src/mage/cards/m/MomoFriendlyFlier.java new file mode 100644 index 00000000000..8ee49906584 --- /dev/null +++ b/Mage.Sets/src/mage/cards/m/MomoFriendlyFlier.java @@ -0,0 +1,132 @@ +package mage.cards.m; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAllTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalCostModificationEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.cost.SpellsCostReductionControllerEffect; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.FilterCard; +import mage.filter.FilterPermanent; +import mage.filter.common.FilterControlledCreaturePermanent; +import mage.filter.predicate.Predicates; +import mage.filter.predicate.mageobject.AbilityPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.Spell; +import mage.watchers.Watcher; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class MomoFriendlyFlier extends CardImpl { + + private static final FilterCard filter = new FilterCard(); + private static final FilterPermanent filter2 = new FilterControlledCreaturePermanent("another creature you control with flying"); + + static { + filter.add(Predicates.not(SubType.LEMUR.getPredicate())); + filter.add(new AbilityPredicate(FlyingAbility.class)); + filter2.add(new AbilityPredicate(FlyingAbility.class)); + } + + public MomoFriendlyFlier(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{W}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.LEMUR); + this.subtype.add(SubType.BAT); + this.subtype.add(SubType.ALLY); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // The first non-Lemur creature spell with flying you cast during each of your turns costs {1} less to cast. + this.addAbility(new SimpleStaticAbility(new ConditionalCostModificationEffect( + new SpellsCostReductionControllerEffect(filter, 1), MomoFriendlyFlierCondition.instance, + "the first non-Lemur creature spell with flying you cast during each of your turns costs {1} less to cast" + )).addHint(MomoFriendlyFlierCondition.getHint()), new MomoFriendlyFlierWatcher()); + + // Whenever another creature you control with flying enters, Momo gets +1/+1 until end of turn. + this.addAbility(new EntersBattlefieldAllTriggeredAbility( + new BoostSourceEffect(1, 1, Duration.EndOfTurn), filter2 + )); + } + + private MomoFriendlyFlier(final MomoFriendlyFlier card) { + super(card); + } + + @Override + public MomoFriendlyFlier copy() { + return new MomoFriendlyFlier(this); + } +} + +enum MomoFriendlyFlierCondition implements Condition { + instance; + private static final Hint hint = new ConditionHint( + instance, "You haven't cast a non-Lemur creature spell with flying during your turn yet" + ); + + public static Hint getHint() { + return hint; + } + + @Override + public boolean apply(Game game, Ability source) { + return game.isActivePlayer(source.getControllerId()) + && !MomoFriendlyFlierWatcher.checkPlayer(game, source); + } +} + +class MomoFriendlyFlierWatcher extends Watcher { + + private final Set set = new HashSet<>(); + + MomoFriendlyFlierWatcher() { + 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.isCreature(game) + && !spell.hasSubtype(SubType.LEMUR, game) + && spell.getAbilities(game).containsClass(FlyingAbility.class)) { + set.add(spell.getControllerId()); + } + } + + @Override + public void reset() { + super.reset(); + set.clear(); + } + + static boolean checkPlayer(Game game, Ability source) { + return game + .getState() + .getWatcher(MomoFriendlyFlierWatcher.class) + .set + .contains(source.getControllerId()); + } +} diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index b1f6b1977e1..3ef0010a083 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -91,6 +91,8 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Master Pakku", 63, Rarity.UNCOMMON, mage.cards.m.MasterPakku.class)); cards.add(new SetCardInfo("Master Piandao", 28, Rarity.UNCOMMON, mage.cards.m.MasterPiandao.class)); cards.add(new SetCardInfo("Merchant of Many Hats", 110, Rarity.COMMON, mage.cards.m.MerchantOfManyHats.class)); + cards.add(new SetCardInfo("Momo, Friendly Flier", 29, Rarity.RARE, mage.cards.m.MomoFriendlyFlier.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Momo, Friendly Flier", 317, Rarity.RARE, mage.cards.m.MomoFriendlyFlier.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Mongoose Lizard", 148, Rarity.COMMON, mage.cards.m.MongooseLizard.class)); cards.add(new SetCardInfo("Mountain", 285, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Mountain", 290, Rarity.LAND, mage.cards.basiclands.Mountain.class, FULL_ART_BFZ_VARIOUS)); From f304cc545aa9fbff8d7cd1bf3119aaa18b1e1e8a Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 16 Aug 2025 10:52:25 -0400 Subject: [PATCH 15/31] [TLA] Implement The Rise of Sozin / Fire Lord Sozin --- Mage.Sets/src/mage/cards/f/FireLordSozin.java | 158 ++++++++++++++++++ .../src/mage/cards/s/SquealingDevil.java | 44 +++-- .../src/mage/cards/t/TheRiseOfSozin.java | 83 +++++++++ .../src/mage/sets/AvatarTheLastAirbender.java | 4 + 4 files changed, 265 insertions(+), 24 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/f/FireLordSozin.java create mode 100644 Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java diff --git a/Mage.Sets/src/mage/cards/f/FireLordSozin.java b/Mage.Sets/src/mage/cards/f/FireLordSozin.java new file mode 100644 index 00000000000..5074b2310fa --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FireLordSozin.java @@ -0,0 +1,158 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.common.delayed.ReflexiveTriggeredAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; +import mage.abilities.keyword.FirebendingAbility; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.filter.FilterCard; +import mage.filter.common.FilterCreatureCard; +import mage.filter.predicate.card.OwnerIdPredicate; +import mage.game.Game; +import mage.players.Player; +import mage.target.common.TargetCardInGraveyard; +import mage.util.CardUtil; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FireLordSozin extends CardImpl { + + public FireLordSozin(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, ""); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.NOBLE); + this.power = new MageInt(5); + this.toughness = new MageInt(5); + this.color.setBlack(true); + this.nightCard = true; + + // Menace + this.addAbility(new MenaceAbility()); + + // Firebending 3 + this.addAbility(new FirebendingAbility(3)); + + // Whenever Fire Lord Sozin deals combat damage to a player, you may pay {X}. When you do, put any number of target creature cards with total mana value X or less from that player's graveyard onto the battlefield under your control. + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility(new FireLordSozinEffect())); + } + + private FireLordSozin(final FireLordSozin card) { + super(card); + } + + @Override + public FireLordSozin copy() { + return new FireLordSozin(this); + } +} + +class FireLordSozinEffect extends OneShotEffect { + + FireLordSozinEffect() { + super(Outcome.Benefit); + staticText = "you may pay {X}. When you do, put any number of target creature cards with " + + "total mana value X or less from that player's graveyard onto the battlefield under your control"; + } + + private FireLordSozinEffect(final FireLordSozinEffect effect) { + super(effect); + } + + @Override + public FireLordSozinEffect copy() { + return new FireLordSozinEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return false; + } + if (controller == null || !controller.chooseUse(Outcome.BoostCreature, "Pay {X}?", source, game)) { + return false; + } + int xValue = controller.announceX(0, Integer.MAX_VALUE, "Announce the value for {X}", game, source, true); + ManaCosts cost = new ManaCostsImpl<>("{X}"); + cost.add(new GenericManaCost(xValue)); + if (!cost.pay(source, game, source, source.getControllerId(), false, null)) { + return false; + } + ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(new ReturnFromGraveyardToBattlefieldTargetEffect(), false); + ability.addTarget(new FireLordSozinTarget((UUID) getValue("damagedPlayer"), xValue)); + game.fireReflexiveTriggeredAbility(ability, source); + return true; + } +} + +class FireLordSozinTarget extends TargetCardInGraveyard { + + private final int xValue; + + private static final FilterCard makeFilter(UUID ownerId, int xValue) { + FilterCard filter = new FilterCreatureCard("creature cards with total mana value " + xValue + " or less from that player's graveyard"); + filter.add(new OwnerIdPredicate(ownerId)); + return filter; + } + + FireLordSozinTarget(UUID ownerId, int xValue) { + super(0, Integer.MAX_VALUE, makeFilter(ownerId, xValue), false); + this.xValue = xValue; + } + + private FireLordSozinTarget(final FireLordSozinTarget target) { + super(target); + this.xValue = target.xValue; + } + + @Override + public FireLordSozinTarget copy() { + return new FireLordSozinTarget(this); + } + + @Override + public boolean canTarget(UUID playerId, UUID id, Ability source, Game game) { + return super.canTarget(playerId, id, source, game) + && CardUtil.checkCanTargetTotalValueLimit(this.getTargets(), id, MageObject::getManaValue, xValue, game); + } + + @Override + public Set possibleTargets(UUID sourceControllerId, Ability source, Game game) { + return CardUtil.checkPossibleTargetsTotalValueLimit( + this.getTargets(), + super.possibleTargets(sourceControllerId, source, game), + MageObject::getManaValue, xValue, game + ); + } + + @Override + public String getMessage(Game game) { + // shows selected total + int selectedValue = this.getTargets().stream() + .map(game::getObject) + .filter(Objects::nonNull) + .mapToInt(MageObject::getManaValue) + .sum(); + return super.getMessage(game) + " (selected total mana value " + selectedValue + ")"; + } +} diff --git a/Mage.Sets/src/mage/cards/s/SquealingDevil.java b/Mage.Sets/src/mage/cards/s/SquealingDevil.java index 556bc576a33..25957faee0a 100644 --- a/Mage.Sets/src/mage/cards/s/SquealingDevil.java +++ b/Mage.Sets/src/mage/cards/s/SquealingDevil.java @@ -1,33 +1,31 @@ - package mage.cards.s; import mage.MageInt; -import mage.abilities.keyword.FearAbility; -import java.util.UUID; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.ManaWasSpentCondition; import mage.abilities.costs.mana.GenericManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.SacrificeSourceUnlessConditionEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.keyword.FearAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; -import mage.constants.SubType; -import mage.abilities.effects.common.continuous.BoostTargetEffect; import mage.constants.Duration; import mage.constants.Outcome; +import mage.constants.SubType; import mage.game.Game; import mage.game.permanent.Permanent; import mage.players.Player; import mage.target.common.TargetCreaturePermanent; import mage.target.targetpointer.FixedTarget; +import java.util.UUID; + /** - * * @author TheElk801 */ public final class SquealingDevil extends CardImpl { @@ -76,24 +74,22 @@ class SquealingDevilEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); - ManaCosts cost = new ManaCostsImpl<>("{X}"); - if (player != null) { - if (player.chooseUse(Outcome.BoostCreature, "Pay " + cost.getText() + "?", source, game)) { - int costX = player.announceX(0, Integer.MAX_VALUE, "Announce the value for {X} (pay to boost)", game, source, true); - cost.add(new GenericManaCost(costX)); - if (cost.pay(source, game, source, source.getControllerId(), false, null)) { - Permanent permanent = game.getPermanent(source.getFirstTarget()); - if (permanent != null && permanent.isCreature(game)) { - ContinuousEffect effect = new BoostTargetEffect(costX, 0, Duration.EndOfTurn); - effect.setTargetPointer(new FixedTarget(permanent, game)); - game.addEffect(effect, source); - return true; - } - return false; - } - } + if (player == null || !player.chooseUse(Outcome.BoostCreature, "Pay {X}?", source, game)) { + return false; } - return false; + int xValue = player.announceX(0, Integer.MAX_VALUE, "Announce the value for {X} (pay to boost)", game, source, true); + ManaCosts cost = new ManaCostsImpl<>("{X}"); + cost.add(new GenericManaCost(xValue)); + if (!cost.pay(source, game, source, source.getControllerId(), false, null)) { + return false; + } + Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source)); + if (permanent == null) { + return false; + } + game.addEffect(new BoostTargetEffect(xValue, 0, Duration.EndOfTurn) + .setTargetPointer(new FixedTarget(permanent, game)), source); + return true; } @Override diff --git a/Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java b/Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java new file mode 100644 index 00000000000..7dc744f65bc --- /dev/null +++ b/Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java @@ -0,0 +1,83 @@ +package mage.cards.t; + +import mage.abilities.Ability; +import mage.abilities.common.SagaAbility; +import mage.abilities.effects.Effects; +import mage.abilities.effects.common.ChooseACardNameEffect; +import mage.abilities.effects.common.DestroyAllEffect; +import mage.abilities.effects.common.ExileSagaAndReturnTransformedEffect; +import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SagaChapter; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.target.common.TargetOpponent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class TheRiseOfSozin extends CardImpl { + + public TheRiseOfSozin(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{4}{B}{B}"); + + this.subtype.add(SubType.SAGA); + this.secondSideCardClazz = mage.cards.f.FireLordSozin.class; + + // (As this Saga enters and after your draw step, add a lore counter.) + SagaAbility sagaAbility = new SagaAbility(this); + + // I -- Destroy all creatures. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_I, new DestroyAllEffect(StaticFilters.FILTER_PERMANENT_CREATURES) + ); + + // II -- 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. + sagaAbility.addChapterEffect( + this, SagaChapter.CHAPTER_II, + new Effects( + new ChooseACardNameEffect(ChooseACardNameEffect.TypeOfName.ALL), new TheRiseOfSozinEffect() + ), new TargetOpponent() + ); + + // III -- Exile this Saga, then return it to the battlefield transformed under your control. + sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_III, new ExileSagaAndReturnTransformedEffect()); + this.addAbility(sagaAbility); + } + + private TheRiseOfSozin(final TheRiseOfSozin card) { + super(card); + } + + @Override + public TheRiseOfSozin copy() { + return new TheRiseOfSozin(this); + } +} + +class TheRiseOfSozinEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect { + + TheRiseOfSozinEffect() { + super(true, "target opponent's", "up to four cards with that name", false, 4); + } + + private TheRiseOfSozinEffect(final TheRiseOfSozinEffect effect) { + super(effect); + } + + @Override + public TheRiseOfSozinEffect copy() { + return new TheRiseOfSozinEffect(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/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 3ef0010a083..10881e59c8a 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -56,6 +56,8 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Epic Downfall", 96, Rarity.UNCOMMON, mage.cards.e.EpicDownfall.class)); cards.add(new SetCardInfo("Fated Firepower", 132, Rarity.MYTHIC, mage.cards.f.FatedFirepower.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Fated Firepower", 341, Rarity.MYTHIC, mage.cards.f.FatedFirepower.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Fire Lord Sozin", 117, Rarity.MYTHIC, mage.cards.f.FireLordSozin.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Fire Lord Sozin", 356, Rarity.MYTHIC, mage.cards.f.FireLordSozin.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Fire Lord Zuko", 221, Rarity.RARE, mage.cards.f.FireLordZuko.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Fire Lord Zuko", 360, Rarity.RARE, mage.cards.f.FireLordZuko.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Fire Nation Attacks", 133, Rarity.UNCOMMON, mage.cards.f.FireNationAttacks.class)); @@ -123,6 +125,8 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Suki, Kyoshi Warrior", 243, Rarity.UNCOMMON, mage.cards.s.SukiKyoshiWarrior.class)); cards.add(new SetCardInfo("Swamp", 284, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Swamp", 289, Rarity.LAND, mage.cards.basiclands.Swamp.class, FULL_ART_BFZ_VARIOUS)); + cards.add(new SetCardInfo("The Rise of Sozin", 117, Rarity.MYTHIC, mage.cards.t.TheRiseOfSozin.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("The Rise of Sozin", 356, Rarity.MYTHIC, mage.cards.t.TheRiseOfSozin.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Toph, the Blind Bandit", 198, Rarity.UNCOMMON, mage.cards.t.TophTheBlindBandit.class)); cards.add(new SetCardInfo("Toph, the First Metalbender", 247, Rarity.RARE, mage.cards.t.TophTheFirstMetalbender.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Toph, the First Metalbender", 353, Rarity.RARE, mage.cards.t.TophTheFirstMetalbender.class, NON_FULL_USE_VARIOUS)); From 910dbe0f0a6e757bdeedbb5c26b0dcf9bc120bdd Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Sat, 16 Aug 2025 12:58:56 -0400 Subject: [PATCH 16/31] fix verify error --- Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java index a9db35f157d..a1b357b008b 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java @@ -24,7 +24,7 @@ public final class AvatarTheLastAirbenderEternal extends ExpansionSet { super("Avatar: The Last Airbender Eternal", "TLE", ExpansionSet.buildDate(2025, 11, 21), SetType.SUPPLEMENTAL); this.blockName = "Avatar: The Last Airbender"; // for sorting in GUI this.rotationSet = true; - this.hasBasicLands = false; + this.hasBasicLands = true; cards.add(new SetCardInfo("Aang's Defense", 211, Rarity.COMMON, mage.cards.a.AangsDefense.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Aang's Defense", 266, Rarity.COMMON, mage.cards.a.AangsDefense.class, NON_FULL_USE_VARIOUS)); From 64558dc5fd1e664b74509be984a9bed7119737d9 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Sat, 16 Aug 2025 18:44:50 -0400 Subject: [PATCH 17/31] fix verify failure --- Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java b/Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java index 7dc744f65bc..78a8b2bd579 100644 --- a/Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java +++ b/Mage.Sets/src/mage/cards/t/TheRiseOfSozin.java @@ -7,6 +7,7 @@ import mage.abilities.effects.common.ChooseACardNameEffect; import mage.abilities.effects.common.DestroyAllEffect; import mage.abilities.effects.common.ExileSagaAndReturnTransformedEffect; import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect; +import mage.abilities.keyword.TransformAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -46,6 +47,7 @@ public final class TheRiseOfSozin extends CardImpl { ); // III -- Exile this Saga, then return it to the battlefield transformed under your control. + this.addAbility(new TransformAbility()); sagaAbility.addChapterEffect(this, SagaChapter.CHAPTER_III, new ExileSagaAndReturnTransformedEffect()); this.addAbility(sagaAbility); } From b6de7f8406a4dbf105140bcfe45b324a4fc0b160 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 09:45:02 -0500 Subject: [PATCH 18/31] remove unused imports from DeathriteShaman --- Mage.Sets/src/mage/cards/d/DeathriteShaman.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/d/DeathriteShaman.java b/Mage.Sets/src/mage/cards/d/DeathriteShaman.java index efa2186a081..88b91646ca8 100644 --- a/Mage.Sets/src/mage/cards/d/DeathriteShaman.java +++ b/Mage.Sets/src/mage/cards/d/DeathriteShaman.java @@ -1,6 +1,5 @@ package mage.cards.d; -import java.util.UUID; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; @@ -16,14 +15,14 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; -import mage.constants.Zone; import mage.filter.FilterCard; import mage.filter.StaticFilters; -import mage.filter.common.FilterCreatureCard; import mage.filter.common.FilterLandCard; import mage.filter.predicate.Predicates; import mage.target.common.TargetCardInGraveyard; +import java.util.UUID; + /** * * @author LevelX2 From 985390b7c340be2211b1f40cdf322b9b7d63197e Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 09:45:47 -0500 Subject: [PATCH 19/31] fix Dark Impostor and add test coverage --- Mage.Sets/src/mage/cards/d/DarkImpostor.java | 2 +- .../cards/single/avr/DarkImpostorTest.java | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/avr/DarkImpostorTest.java diff --git a/Mage.Sets/src/mage/cards/d/DarkImpostor.java b/Mage.Sets/src/mage/cards/d/DarkImpostor.java index 34ca02b5c1d..cc24ed51e4e 100644 --- a/Mage.Sets/src/mage/cards/d/DarkImpostor.java +++ b/Mage.Sets/src/mage/cards/d/DarkImpostor.java @@ -102,7 +102,7 @@ class DarkImpostorContinuousEffect extends ContinuousEffectImpl { @Override public boolean apply(Game game, Ability source) { Permanent permanent = source.getSourcePermanentIfItStillExists(game); - ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source)); + ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source, 1)); if (permanent == null || exileZone == null || exileZone.isEmpty()) { return false; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/DarkImpostorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/DarkImpostorTest.java new file mode 100644 index 00000000000..6be3f478b97 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/avr/DarkImpostorTest.java @@ -0,0 +1,53 @@ +package org.mage.test.cards.single.avr; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class DarkImpostorTest extends CardTestPlayerBase { + + /* + Dark Impostor + {2}{B} + Creature — Vampire Assassin + + {4}{B}{B}: Exile target creature and put a +1/+1 counter on this creature. + + This creature has all activated abilities of all creature cards exiled with it. + 2/2 + */ + public static final String darkImposter = "Dark Impostor"; + /* + Deathrite Shaman + {B/G} + Creature — Elf Shaman + + {T}: Exile target land card from a graveyard. Add one mana of any color. + + {B}, {T}: Exile target instant or sorcery card from a graveyard. Each opponent loses 2 life. + + {G}, {T}: Exile target creature card from a graveyard. You gain 2 life. + + 1/2 + */ + public static final String deathriteShaman = "Deathrite Shaman"; + + @Test + public void testDarkImpostor() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, darkImposter); + addCard(Zone.BATTLEFIELD, playerA, deathriteShaman); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 6); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{B}{B}"); + addTarget(playerA, deathriteShaman); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAbilityCount(playerA, darkImposter, SimpleActivatedAbility.class, 4); // own ability + 3 other from deathrite + } +} From abe8a4356e8d8df781cd5820303466026f6db435 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 09:46:17 -0500 Subject: [PATCH 20/31] increase test coverage for sacrifice effects --- .../cards/single/lci/DireBlunderbussTest.java | 44 ++++++++++++++ .../single/mir/PhyrexianDreadnoughtTest.java | 59 +++++++++++++++++++ .../single/one/NahirisSacrificeTest.java | 57 ++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/lci/DireBlunderbussTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mir/PhyrexianDreadnoughtTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/one/NahirisSacrificeTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/DireBlunderbussTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/DireBlunderbussTest.java new file mode 100644 index 00000000000..2acd66fb4de --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/DireBlunderbussTest.java @@ -0,0 +1,44 @@ +package org.mage.test.cards.single.lci; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +public class DireBlunderbussTest extends CardTestPlayerBase { + + /* + Dire Blunderbuss + Color Indicator: RedArtifact — Equipment + + Equipped creature gets +3/+0 and has “Whenever this creature attacks, you may sacrifice an artifact other than Dire Blunderbuss. When you do, this creature deals damage equal to its power to target creature.” + + Equip {1} + */ + private static final String direBlunderBuss = "Dire Blunderbuss"; + + @Test + public void DireBlunderbussTest() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest"); + addCard(Zone.BATTLEFIELD, playerA, direBlunderBuss); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears"); + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears@bearsB"); + addCard(Zone.BATTLEFIELD, playerA, "Tormod's Crypt"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", "Balduvian Bears"); + + attack(1, playerA, "Balduvian Bears"); + setChoice(playerA, true); + setChoice(playerA, "Tormod's Crypt"); + addTarget(playerA, "@bearsB"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Balduvian Bears", 1); + assertLife(playerB, 20 - 2 - 3); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/PhyrexianDreadnoughtTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/PhyrexianDreadnoughtTest.java new file mode 100644 index 00000000000..db3d1766d7a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mir/PhyrexianDreadnoughtTest.java @@ -0,0 +1,59 @@ +package org.mage.test.cards.single.mir; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.player.TestPlayer; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class PhyrexianDreadnoughtTest extends CardTestPlayerBase { + + /* + Phyrexian Dreadnought + {1} + Artifact Creature — Phyrexian Dreadnought + + Trample + + When this creature enters, sacrifice it unless you sacrifice any number of creatures with total power 12 or greater. + + 12/12 + */ + private static final String phyrexianDreadnought = "Phyrexian Dreadnought"; + + @Test + public void testPhyrexianDreadnoughtCanPay() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, phyrexianDreadnought); + addCard(Zone.BATTLEFIELD, playerA, phyrexianDreadnought + "@sacTarget"); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, phyrexianDreadnought); + setChoice(playerA, true); + setChoice(playerA, "@sacTarget"); + setChoice(playerA, TestPlayer.CHOICE_SKIP); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, phyrexianDreadnought, 1); + assertGraveyardCount(playerA, phyrexianDreadnought, 1); + } + + @Test + public void testPhyrexianDreadnoughtCantPay() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, phyrexianDreadnought); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, phyrexianDreadnought); + + setStopAt(1, PhaseStep.END_TURN); + setChoice(playerA, false); + execute(); + + assertGraveyardCount(playerA, phyrexianDreadnought, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/one/NahirisSacrificeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/one/NahirisSacrificeTest.java new file mode 100644 index 00000000000..24087ff3541 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/one/NahirisSacrificeTest.java @@ -0,0 +1,57 @@ +package org.mage.test.cards.single.one; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +public class NahirisSacrificeTest extends CardTestPlayerBase { + + /* + Nahiri's Sacrifice + {1}{R} + Sorcery + + As an additional cost to cast this spell, sacrifice an artifact or creature with mana value X. + + Nahiri’s Sacrifice deals X damage divided as you choose among any number of target creatures. + */ + private static final String nahirisSacrifice = "Nahiri's Sacrifice"; + private static final String balduvianBears = "Balduvian Bears"; + @Test + public void testNahirisSacrifice() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, nahirisSacrifice); + addCard(Zone.BATTLEFIELD, playerA, balduvianBears); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerB, balduvianBears + "@bearsB"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nahirisSacrifice, "@bearsB"); + setChoice(playerA, "X=2"); + setChoice(playerA, balduvianBears); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, balduvianBears, 1); + assertGraveyardCount(playerA, nahirisSacrifice, 1); + } + + @Test + public void testNahirisSacrificePrevented() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, nahirisSacrifice); + addCard(Zone.BATTLEFIELD, playerA, balduvianBears); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerB, balduvianBears + "@bearsB"); + addCard(Zone.BATTLEFIELD, playerB, "Yasharn, Implacable Earth"); + + checkPlayableAbility("Can't cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + nahirisSacrifice, false); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + } +} From e6da7ed600f84c97cf3884f4b24435b8286b5e80 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 10:57:43 -0500 Subject: [PATCH 21/31] fix Alpine Moon incorrectly removing abilities in layer 4 --- Mage.Sets/src/mage/cards/a/AlpineMoon.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AlpineMoon.java b/Mage.Sets/src/mage/cards/a/AlpineMoon.java index da13a2bff44..6c4ab3c4649 100644 --- a/Mage.Sets/src/mage/cards/a/AlpineMoon.java +++ b/Mage.Sets/src/mage/cards/a/AlpineMoon.java @@ -57,6 +57,12 @@ class AlpineMoonEffect extends ContinuousEffectImpl { this.staticText = "lands your opponents control with the chosen name " + "lose all land types and abilities, " + "and they gain \"{T}: Add one mana of any color.\""; + addDependedToType(DependencyType.BecomeMountain); + addDependedToType(DependencyType.BecomeForest); + addDependedToType(DependencyType.BecomeIsland); + addDependedToType(DependencyType.BecomeSwamp); + addDependedToType(DependencyType.BecomePlains); + addDependedToType(DependencyType.BecomeNonbasicLand); } private AlpineMoonEffect(final AlpineMoonEffect effect) { @@ -84,9 +90,6 @@ class AlpineMoonEffect extends ContinuousEffectImpl { for (Permanent land : game.getBattlefield().getActivePermanents(filter2, source.getControllerId(), game)) { switch (layer) { case TypeChangingEffects_4: - // 305.7 Note that this doesn't remove any abilities that were granted to the land by other effects - // So the ability removing has to be done before Layer 6 - land.removeAllAbilities(source.getSourceId(), game); land.removeAllSubTypes(game, SubTypeSet.NonBasicLandType); break; case AbilityAddingRemovingEffects_6: From 4410374840f2d96dba2f05ce9135df7caa4b27c5 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 10:58:32 -0500 Subject: [PATCH 22/31] fix An-Havva Constable passing incorrect id to battlefield count function --- Mage.Sets/src/mage/cards/a/AnHavvaConstable.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AnHavvaConstable.java b/Mage.Sets/src/mage/cards/a/AnHavvaConstable.java index f077d739041..8de16885a89 100644 --- a/Mage.Sets/src/mage/cards/a/AnHavvaConstable.java +++ b/Mage.Sets/src/mage/cards/a/AnHavvaConstable.java @@ -1,7 +1,6 @@ package mage.cards.a; -import java.util.UUID; import mage.MageInt; import mage.MageObject; import mage.ObjectColor; @@ -16,6 +15,8 @@ import mage.filter.predicate.mageobject.ColorPredicate; import mage.game.Game; import mage.players.Player; +import java.util.UUID; + /** * * @author fireshoes @@ -29,7 +30,7 @@ public final class AnHavvaConstable extends CardImpl { this.toughness = new MageInt(1); // An-Havva Constable's toughness is equal to 1 plus the number of green creatures on the battlefield. - this.addAbility(new SimpleStaticAbility(new AnHavvaConstableEffect())); + this.addAbility(new SimpleStaticAbility(Zone.ALL, new AnHavvaConstableEffect())); } private AnHavvaConstable(final AnHavvaConstable card) { @@ -72,7 +73,7 @@ class AnHavvaConstableEffect extends ContinuousEffectImpl { FilterCreaturePermanent filter = new FilterCreaturePermanent("green creatures"); filter.add(new ColorPredicate(ObjectColor.GREEN)); - int numberOfGreenCreatures = game.getBattlefield().count(filter, source.getSourceId(), source, game); + int numberOfGreenCreatures = game.getBattlefield().count(filter, source.getControllerId(), source, game); mageObject.getToughness().setModifiedBaseValue(1 + numberOfGreenCreatures); From c16b3d6056a3542768c2c213b5de75d7f342227a Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 10:59:01 -0500 Subject: [PATCH 23/31] add tests to increase coverage --- .../AddCardSubtypeAllEffectTest.java | 55 ++++++++++++ .../continuous/BecomesColorEffectTest.java | 69 +++++++++++++++ .../continuous/BecomesCreatureEffectTest.java | 87 +++++++++++++++++++ .../BecomesCreatureIfVehicleEffectTest.java | 45 ++++++++++ .../single/_5ed/AnHavvaConstableTest.java | 51 +++++++++++ .../single/c13/ActOfAuthorityEffectTest.java | 34 ++++++++ .../single/dft/AatchikEmeraldRadianTest.java | 57 ++++++++++++ .../cards/single/fin/AettirAndPriwenTest.java | 66 ++++++++++++++ .../single/lci/AbuelosAwakeningTest.java | 78 +++++++++++++++++ .../test/cards/single/m19/AlpineMoonTest.java | 63 ++++++++++++++ .../single/mh2/AeveProgenitorOozeTest.java | 60 +++++++++++++ .../test/cards/single/tmp/AlurenTest.java | 45 ++++++++++ 12 files changed, 710 insertions(+) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/AddCardSubtypeAllEffectTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesColorEffectTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesCreatureEffectTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesCreatureIfVehicleEffectTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/_5ed/AnHavvaConstableTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/c13/ActOfAuthorityEffectTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/dft/AatchikEmeraldRadianTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/fin/AettirAndPriwenTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/lci/AbuelosAwakeningTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/m19/AlpineMoonTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/AeveProgenitorOozeTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/AlurenTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AddCardSubtypeAllEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AddCardSubtypeAllEffectTest.java new file mode 100644 index 00000000000..6b52fd0918c --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/AddCardSubtypeAllEffectTest.java @@ -0,0 +1,55 @@ +package org.mage.test.cards.continuous; + +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AddCardSubtypeAllEffectTest extends CardTestPlayerBase { + + /* + Kudo, King Among Bears + {G}{W} + Legendary Creature — Bear + Other creatures have base power and toughness 2/2 and are Bears in addition to their other types. + 2/2 + */ + private static final String kudo = "Kudo, King Among Bears"; + + /* + Fugitive Wizard + {U} + Creature — Human Wizard + 1/1 + */ + private static final String fugitive = "Fugitive Wizard"; + + @Test + public void testAddCardSubtypeAllEffect() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, fugitive, 3); + addCard(Zone.BATTLEFIELD, playerB, fugitive, 3); + addCard(Zone.BATTLEFIELD, playerA, kudo); + addCard(Zone.BATTLEFIELD, playerA, "Savannah", 2); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + for (Permanent permanent : currentGame.getBattlefield().getAllActivePermanents()) { + if (permanent.getName().equals(fugitive)) { + assertTrue(permanent.hasSubtype(SubType.BEAR, currentGame)); + assertTrue(permanent.hasSubtype(SubType.WIZARD, currentGame)); + assertTrue(permanent.hasSubtype(SubType.HUMAN, currentGame)); + assertEquals(2, permanent.getPower().getModifiedBaseValue()); + assertEquals(2, permanent.getToughness().getModifiedBaseValue()); + } + } + + assertSubtype(kudo, SubType.BEAR); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesColorEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesColorEffectTest.java new file mode 100644 index 00000000000..0002250c017 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesColorEffectTest.java @@ -0,0 +1,69 @@ +package org.mage.test.cards.continuous; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class BecomesColorEffectTest extends CardTestPlayerBase { + + /* + Ancient Kavu + {3}{R} + Creature — Kavu + {2}: This creature becomes colorless until end of turn. + Those with the ability to change their nature survived Phyrexia’s biological attacks. Everything else died. + 3/3 + */ + String kavu = "Ancient Kavu"; + /* + Alchor's Tomb + {4} + Artifact + {2}, {T}: Target permanent you control becomes the color of your choice. (This effect lasts indefinitely.) + */ + String alchorsTomb = "Alchor's Tomb"; + /* + + */ + + @Test + public void testBecomesColorSource() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, kavu); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + checkColor("Ancient Kavu is red", 1, PhaseStep.PRECOMBAT_MAIN, playerA, kavu, "R", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: {this}"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkColor("Ancient Kavu is colorless", 1, PhaseStep.PRECOMBAT_MAIN, playerA, kavu, "C", true); + checkColor("Ancient Kavu is red again", 2, PhaseStep.PRECOMBAT_MAIN, playerA, kavu, "R", true); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + } + + @Test + public void testBecomesColorTarget() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, kavu); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerA, alchorsTomb); + + checkColor("Ancient Kavu is red", 1, PhaseStep.PRECOMBAT_MAIN, playerA, kavu, "R", true); + // make Ancient Kavu green + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, {T}: Target permanent", kavu); + setChoice(playerA, "Green"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkColor("Ancient Kavu is green", 1, PhaseStep.PRECOMBAT_MAIN, playerA, kavu, "G", true); + checkColor("Ancient Kavu is still green the following turn", 2, PhaseStep.PRECOMBAT_MAIN, playerA, kavu, "G", true); + // activate Ancient Kavu's ability to override green color until end of turn + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}: {this}"); + checkColor("Ancient Kavu is colorless", 3, PhaseStep.POSTCOMBAT_MAIN, playerA, kavu, "C", true); + // next turn it should be green again + checkColor("Ancient Kavu is green again", 4, PhaseStep.PRECOMBAT_MAIN, playerA, kavu, "G", true); + + setStopAt(4, PhaseStep.BEGIN_COMBAT); + execute(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesCreatureEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesCreatureEffectTest.java new file mode 100644 index 00000000000..9b9cff0f81f --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesCreatureEffectTest.java @@ -0,0 +1,87 @@ +package org.mage.test.cards.continuous; + +import mage.ObjectColor; +import mage.constants.*; +import mage.filter.FilterPermanent; +import mage.filter.predicate.mageobject.PowerPredicate; +import mage.filter.predicate.mageobject.ToughnessPredicate; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import java.util.Collections; + +public class BecomesCreatureEffectTest extends CardTestPlayerBase { + /* + Ambush Commander + {3}{G}{G} + Creature — Elf + Forests you control are 1/1 green Elf creatures that are still lands. + {1}{G}, Sacrifice an Elf: Target creature gets +3/+3 until end of turn. + 2/2 + */ + String ambushCommander = "Ambush Commander"; + /* + Dryad Arbor + Land Creature — Forest Dryad + 1/1 + */ + String dryadArbor = "Dryad Arbor"; + /* + Frogify + {1}{U} + Enchantment — Aura + Enchant creature + Enchanted creature loses all abilities and is a blue Frog creature with base power and toughness 1/1. + (It loses all other card types and creature types.) + */ + String frogify = "Frogify"; + @Test + public void testBecomesCreatureAllEffect() { + FilterPermanent filter = new FilterPermanent(); + filter.add(CardType.CREATURE.getPredicate()); + filter.add(SubType.ELF.getPredicate()); + filter.add(new PowerPredicate(ComparisonType.EQUAL_TO, 1)); + filter.add(new ToughnessPredicate(ComparisonType.EQUAL_TO, 1)); + + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, dryadArbor); + addCard(Zone.HAND, playerA, ambushCommander); + + runCode("Check forests are not 1/1 Elves", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + int numElves = game.getBattlefield().getActivePermanents(filter, player.getId(), game).size(); + Assert.assertEquals("No 1/1 elves should be present", 0, numElves); + }); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, ambushCommander); + runCode("Check forests are 1/1 Elves", 1, PhaseStep.BEGIN_COMBAT, playerA, (info, player, game) -> { + int numElves = game.getBattlefield().getActivePermanents(filter, player.getId(), game).size(); + // 5 forests + dryad arbor + Assert.assertEquals("There should be 6 1/1 elves present", 6, numElves); + }); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void testBecomesCreatureAttachedEffect() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, dryadArbor); + addCard(Zone.HAND, playerA, frogify); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, frogify, dryadArbor); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAbilities(playerA, dryadArbor, Collections.emptyList()); + assertPowerToughness(playerA, dryadArbor, 1, 1); + assertType(dryadArbor, CardType.CREATURE, SubType.FROG); + assertNotSubtype(dryadArbor, SubType.DRYAD); + assertNotType(dryadArbor, CardType.LAND); + assertColor(playerA, dryadArbor, ObjectColor.BLUE, true); + assertColor(playerA, dryadArbor, ObjectColor.GREEN, false); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesCreatureIfVehicleEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesCreatureIfVehicleEffectTest.java new file mode 100644 index 00000000000..a95936d743a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/BecomesCreatureIfVehicleEffectTest.java @@ -0,0 +1,45 @@ +package org.mage.test.cards.continuous; + +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class BecomesCreatureIfVehicleEffectTest extends CardTestPlayerBase { + + /* + Aerial Modification + {4}{W} + Enchantment — Aura + Enchant creature or Vehicle + As long as enchanted permanent is a Vehicle, it’s a creature in addition to its other types. + Enchanted creature gets +2/+2 and has flying. + */ + String aerialMod = "Aerial Modification"; + /* + Goliath Truck + {4} + Artifact — Vehicle + Stowage — Whenever this Vehicle attacks, put two +1/+1 counters on another target attacking creature. + Crew 2 (Tap any number of creatures you control with total power 2 or more: This Vehicle becomes an artifact creature until end of turn.) + 4/4 + */ + String goliathTruck = "Goliath Truck"; + + @Test + public void testBecomesCreatureIfVehicleEffect() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, goliathTruck); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.HAND, playerA, aerialMod); + + checkType("Goliath Truck is not a creature", 1, PhaseStep.PRECOMBAT_MAIN, playerA, goliathTruck, CardType.CREATURE, false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aerialMod, goliathTruck); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertType(goliathTruck, CardType.CREATURE, true); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/_5ed/AnHavvaConstableTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/_5ed/AnHavvaConstableTest.java new file mode 100644 index 00000000000..d6485265308 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/_5ed/AnHavvaConstableTest.java @@ -0,0 +1,51 @@ +package org.mage.test.cards.single._5ed; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class AnHavvaConstableTest extends CardTestPlayerBase { + + /* + An-Havva Constable + {1}{G}{G} + Creature — Human + An-Havva Constable’s toughness is equal to 1 plus the number of green creatures on the battlefield. + 2/1+* + */ + private static final String constable = "An-Havva Constable"; + /* + Bear Cub + {1}{G} + Creature — Bear + 2/2 + */ + private static final String cub = "Bear Cub"; + @Test + public void testAnHavva() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, constable); + addCard(Zone.HAND, playerA, constable); + addCard(Zone.BATTLEFIELD, playerA, cub, 2); + addCard(Zone.BATTLEFIELD, playerB, cub, 3); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + + checkPT("An-Havva Constable has toughness 7", 1, PhaseStep.PRECOMBAT_MAIN, playerA, constable, 2, 1 + 6); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", cub); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPT("An-Havva Constable has toughness 6", 1, PhaseStep.PRECOMBAT_MAIN, playerA, constable, 2, 1 + 5); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + playerA.getHand().getCards(currentGame).stream() + .filter(card -> card.getName().equals(constable)) + .findFirst(). + ifPresent(card -> Assert.assertEquals(6, card.getToughness().getValue())); + + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/c13/ActOfAuthorityEffectTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/c13/ActOfAuthorityEffectTest.java new file mode 100644 index 00000000000..05ce5bdb0d4 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/c13/ActOfAuthorityEffectTest.java @@ -0,0 +1,34 @@ +package org.mage.test.cards.single.c13; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class ActOfAuthorityEffectTest extends CardTestPlayerBase { + /* + Act of Authority + {1}{W}{W} + Enchantment + When this enchantment enters, you may exile target artifact or enchantment. + At the beginning of your upkeep, you may exile target artifact or enchantment. If you do, its controller gains control of this enchantment. + */ + private static final String actOfAuthority = "Act of Authority"; + + @Test + public void testActOfAuthority() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, actOfAuthority); + addCard(Zone.BATTLEFIELD, playerB, actOfAuthority + "@actB"); + + setChoice(playerA, true); // upkeep + addTarget(playerA, "@actB"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, actOfAuthority, 1); + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/AatchikEmeraldRadianTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/AatchikEmeraldRadianTest.java new file mode 100644 index 00000000000..bb03167ce2a --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dft/AatchikEmeraldRadianTest.java @@ -0,0 +1,57 @@ +package org.mage.test.cards.single.dft; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class AatchikEmeraldRadianTest extends CardTestPlayerBase { + + /* + Aatchik, Emerald Radian + {3}{B}{B}{G} + Legendary Creature — Insect Druid + When Aatchik enters, create a 1/1 green Insect creature token for each artifact and/or creature card in your graveyard. + Whenever another Insect you control dies, put a +1/+1 counter on Aatchik. Each opponent loses 1 life. + 3/3 + */ + private static final String aatchik = "Aatchik, Emerald Radian"; + + /* + Springheart Nantuko + {1}{G} + Enchantment Creature — Insect Monk + Bestow {1}{G} + Enchanted creature gets +1/+1. + Landfall — Whenever a land you control enters, you may pay {1}{G} if this permanent is attached to a creature you control. + If you do, create a token that’s a copy of that creature. If you didn’t create a token this way, create a 1/1 green Insect creature token. + 1/1 + */ + private static final String nantuko = "Springheart Nantuko"; + + @Test + public void testOpponentCreatingTokens() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, aatchik); + addCard(Zone.BATTLEFIELD, playerB, aatchik); + addCard(Zone.GRAVEYARD, playerA, aatchik); + addCard(Zone.GRAVEYARD, playerB, aatchik); + addCard(Zone.BATTLEFIELD, playerA, "Bayou", 9); + addCard(Zone.HAND, playerA, nantuko); + addCard(Zone.HAND, playerA, "Bayou"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, nantuko + " using bestow"); + addTarget(playerA, aatchik); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bayou"); + setChoice(playerA, true); + setChoice(playerA, aatchik); + setChoice(playerA, "When {this} enters"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTokenCount(playerB, "Insect Token", 0); + assertTokenCount(playerA, "Insect Token", 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/fin/AettirAndPriwenTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/fin/AettirAndPriwenTest.java new file mode 100644 index 00000000000..c5bd9d4e334 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/fin/AettirAndPriwenTest.java @@ -0,0 +1,66 @@ +package org.mage.test.cards.single.fin; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class AettirAndPriwenTest extends CardTestPlayerBase { + + /* + Aettir and Priwen + {6} + Legendary Artifact — Equipment + Equipped creature has base power and toughness X/X, where X is your life total. + Equip {5} + */ + private static final String aettir = "Aettir and Priwen"; + /* + Bear Cub + {1}{G} + Creature — Bear + 2/2 + */ + private static final String cub = "Bear Cub"; + /* + Lightning Bolt + {R} + Instant + Lightning Bolt deals 3 damage to any target. + */ + public static final String bolt = "Lightning Bolt"; + + @Test + public void testAettirAndPriwen() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, aettir); + addCard(Zone.BATTLEFIELD, playerA, cub); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 7); + addCard(Zone.HAND, playerA, bolt, 2); + + checkPowerToughness(2, cub, 1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{5}: Equip"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPowerToughness(20, cub, 1, PhaseStep.PRECOMBAT_MAIN); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, bolt, playerA); + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + checkPowerToughness(20 - 3, cub, 1, PhaseStep.POSTCOMBAT_MAIN); + + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, bolt, playerA); + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + checkPowerToughness(20 - 3 * 2, cub, 1, PhaseStep.POSTCOMBAT_MAIN); + + checkPowerToughness(20 - 3 * 2, cub, 2, PhaseStep.PRECOMBAT_MAIN); + } + + void checkPowerToughness(int xValue, String name, int turnNum, PhaseStep phaseStep) { + runCode("Checking P/T is " + xValue, turnNum, phaseStep, playerA, (info, player, game) -> { + Permanent permanent = getPermanent(name, player); + Assert.assertEquals(xValue, permanent.getPower().getModifiedBaseValue()); + Assert.assertEquals(xValue, permanent.getToughness().getModifiedBaseValue()); + }); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/AbuelosAwakeningTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/AbuelosAwakeningTest.java new file mode 100644 index 00000000000..569db29afcf --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/AbuelosAwakeningTest.java @@ -0,0 +1,78 @@ +package org.mage.test.cards.single.lci; + +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class AbuelosAwakeningTest extends CardTestPlayerBase { + + /* + Abuelo's Awakening + {X}{3}{W} + Sorcery + Return target artifact or non-Aura enchantment card from your graveyard to the battlefield with X additional +1/+1 counters on it. + It’s a 1/1 Spirit creature with flying in addition to its other types. + */ + public static final String abuelosAwakening = "Abuelo's Awakening"; + /* + Talisman of Progress + {2} + Artifact + {T}: Add {C}. + {T}: Add {W} or {U}. This artifact deals 1 damage to you. + */ + public static final String talisman = "Talisman of Progress"; + /* + Lightning Bolt + {R} + Instant + Lightning Bolt deals 3 damage to any target. + */ + public static final String bolt = "Lightning Bolt"; + + @Test + public void testAbuelosAwakening() { + setStrictChooseMode(true); + + addCard(Zone.GRAVEYARD, playerA, talisman); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + addCard(Zone.HAND, playerA, abuelosAwakening); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, abuelosAwakening, talisman); + setChoiceAmount(playerA, 2); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertCounterCount(playerA, talisman, CounterType.P1P1, 2); + assertType(talisman, CardType.CREATURE, true); + assertSubtype(talisman, SubType.SPIRIT); + assertBasePowerToughness(playerA, talisman, 1, 1); + } + + @Test + public void testAbuelosAwakeningDies() { + setStrictChooseMode(true); + + addCard(Zone.GRAVEYARD, playerA, talisman); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + addCard(Zone.HAND, playerA, abuelosAwakening); + addCard(Zone.HAND, playerB, bolt); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, abuelosAwakening, talisman); + setChoiceAmount(playerA, 2); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, bolt, talisman); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, talisman, 0); + assertGraveyardCount(playerA, talisman, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/m19/AlpineMoonTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/m19/AlpineMoonTest.java new file mode 100644 index 00000000000..6f1399f9e03 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/m19/AlpineMoonTest.java @@ -0,0 +1,63 @@ +package org.mage.test.cards.single.m19; + +import mage.abilities.mana.AnyColorManaAbility; +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class AlpineMoonTest extends CardTestPlayerBase { + /* + Alpine Moon + {R} + Enchantment + As this enchantment enters, choose a nonbasic land card name. + Lands your opponents control with the chosen name lose all land types and abilities, and they gain “{T}: Add one mana of any color.” + */ + private static final String alpine = "Alpine Moon"; + /* + Urborg, Tomb of Yawgmoth + Legendary Land + Each land is a Swamp in addition to its other land types. + */ + private static final String urborg = "Urborg, Tomb of Yawgmoth"; + + @Test + public void testAlpineMoonAfterUrborg() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, urborg); + addCard(Zone.BATTLEFIELD, playerB, alpine); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + + setChoice(playerB, urborg); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertNotSubtype(urborg, SubType.SWAMP); + assertSubtype("Mountain", SubType.SWAMP); + assertSubtype("Island", SubType.SWAMP); + assertAbility(playerA, urborg, new AnyColorManaAbility(), true); + } + + @Test + public void testAlpineMoonBeforeUrborg() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerB, urborg); + addCard(Zone.BATTLEFIELD, playerA, alpine); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerB, "Island"); + + setChoice(playerA, urborg); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertNotSubtype(urborg, SubType.SWAMP); + assertSubtype("Mountain", SubType.SWAMP); + assertSubtype("Island", SubType.SWAMP); + assertAbility(playerB, urborg, new AnyColorManaAbility(), true); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/AeveProgenitorOozeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/AeveProgenitorOozeTest.java new file mode 100644 index 00000000000..f60dc9fa1e6 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh2/AeveProgenitorOozeTest.java @@ -0,0 +1,60 @@ +package org.mage.test.cards.single.mh2; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentToken; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class AeveProgenitorOozeTest extends CardTestPlayerBase { + + /* + Aeve, Progenitor Ooze + {2}{G}{G}{G} + Legendary Creature — Ooze + Storm (When you cast this spell, copy it for each spell cast before it this turn. Copies become tokens.) + Aeve isn’t legendary if it’s a token. + Aeve enters with a +1/+1 counter on it for each other Ooze you control. + 2/2 + */ + private static final String aeve = "Aeve, Progenitor Ooze"; + /* + Lightning Bolt + {R} + Instant + Lightning Bolt deals 3 damage to any target. + */ + public static final String bolt = "Lightning Bolt"; + + @Test + public void testAeve() { + setStrictChooseMode(true); + addCard(Zone.HAND, playerA, aeve); + addCard(Zone.HAND, playerA, bolt); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 5); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, bolt, playerB); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, aeve); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, aeve, 2); + assertTokenCount(playerA, aeve, 1); + for (Permanent permanent : currentGame.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURE, playerA.getId(), currentGame)) { + if (permanent.getName().equals(aeve)) { + if (permanent instanceof PermanentToken) { + Assert.assertEquals(0, permanent.getCounters(currentGame).getCount(CounterType.P1P1)); + } else { + Assert.assertEquals(1, permanent.getCounters(currentGame).getCount(CounterType.P1P1)); + } + } + } + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/AlurenTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/AlurenTest.java new file mode 100644 index 00000000000..b1ded1979b0 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/AlurenTest.java @@ -0,0 +1,45 @@ +package org.mage.test.cards.single.tmp; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class AlurenTest extends CardTestPlayerBase { + + /* + Aluren + {2}{G}{G} + Enchantment + Any player may cast creature spells with mana value 3 or less without paying their mana costs and as though they had flash. + */ + private static final String aluren = "Aluren"; + + /* + Bear Cub + {1}{G} + Creature — Bear + 2/2 + */ + private static final String cub = "Bear Cub"; + + @Test + public void testAluren() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, aluren); + addCard(Zone.HAND, playerA, cub); + addCard(Zone.HAND, playerB, cub); + + castSpell(1, PhaseStep.UPKEEP, playerA, cub); + setChoice(playerA, "Cast without paying its mana cost"); + + castSpell(1, PhaseStep.END_TURN, playerB, cub); + setChoice(playerB, "Cast without paying its mana cost"); + + setStopAt(2, PhaseStep.UPKEEP); + execute(); + + assertPermanentCount(playerA, cub, 1); + assertPermanentCount(playerB, cub, 1); + } +} From b0ee19d78215b7ec0e5272164c033ba6a6e96572 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 12:26:23 -0500 Subject: [PATCH 24/31] fix Stalwart Successor checking permanents entering --- .../src/mage/cards/s/StalwartSuccessor.java | 12 ++++- .../single/tdm/StalwartSuccessorTest.java | 48 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/StalwartSuccessorTest.java diff --git a/Mage.Sets/src/mage/cards/s/StalwartSuccessor.java b/Mage.Sets/src/mage/cards/s/StalwartSuccessor.java index ae0fbeacb18..5497da547fb 100644 --- a/Mage.Sets/src/mage/cards/s/StalwartSuccessor.java +++ b/Mage.Sets/src/mage/cards/s/StalwartSuccessor.java @@ -78,14 +78,19 @@ class StalwartSuccessorTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { + int zccOffset = 0; Permanent permanent = game.getPermanent(event.getTargetId()); + if (permanent == null) { + permanent = game.getPermanentEntering(event.getTargetId()); + zccOffset = 1; + } if (permanent == null || !permanent.isCreature(game) || !permanent.isControlledBy(getControllerId()) || !StalwartSuccessorWatcher.checkCreature(permanent, event, game)) { return false; } - this.getEffects().setTargetPointer(new FixedTarget(permanent, game)); + this.getEffects().setTargetPointer(new FixedTarget(permanent.getId(), permanent.getZoneChangeCounter(game) + zccOffset)); return true; } } @@ -101,6 +106,11 @@ class StalwartSuccessorWatcher extends Watcher { @Override public void watch(GameEvent event, Game game) { if (event.getType() == GameEvent.EventType.COUNTERS_ADDED) { + if (game.getPermanent(event.getTargetId()) == null) { + // permanent entering + Permanent permanent = game.getPermanentEntering(event.getTargetId()); + map.putIfAbsent(new MageObjectReference(event.getTargetId(), permanent.getZoneChangeCounter(game) + 1, game), event.getId()); + } map.putIfAbsent(new MageObjectReference(event.getTargetId(), game), event.getId()); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/StalwartSuccessorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/StalwartSuccessorTest.java new file mode 100644 index 00000000000..343dcf899fc --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tdm/StalwartSuccessorTest.java @@ -0,0 +1,48 @@ +package org.mage.test.cards.single.tdm; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class StalwartSuccessorTest extends CardTestPlayerBase { + + /* + Stalwart Successor + {1}{B}{G} + Creature — Human Warrior + + Menace (This creature can’t be blocked except by two or more creatures.) + + Whenever one or more counters are put on a creature you control, if it’s the first time counters have been put on that creature this turn, put a +1/+1 counter on that creature. + + 3/2 + */ + private static final String stalwartSuccessor = "Stalwart Successor"; + /* + Purestrain Genestealer + {2}{G} + Creature — Tyranid + + This creature enters with two +1/+1 counters on it. + + Vanguard Species — Whenever this creature attacks, you may remove a +1/+1 counter from it. If you do, search your library for a basic land card, put it onto the battlefield tapped, then shuffle. + + 1/1 + */ + private static final String purestrainGenestealer = "Purestrain Genestealer"; + @Test + public void testStalwartSuccessor() { + + addCard(Zone.BATTLEFIELD, playerA, stalwartSuccessor); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.HAND, playerA, purestrainGenestealer); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, purestrainGenestealer); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertCounterCount(playerA, purestrainGenestealer, CounterType.P1P1, 2 + 1); + } +} From 0651825e6db20df9d1c9b09c03c17ce97c9f7ab7 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 12:26:50 -0500 Subject: [PATCH 25/31] add test coverage for Jaya, Fiery Negotiator emblem --- .../mage/test/cards/emblems/EmblemsTest.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/emblems/EmblemsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/emblems/EmblemsTest.java index a1587ff240e..5260c13070f 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/emblems/EmblemsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/emblems/EmblemsTest.java @@ -157,4 +157,29 @@ public class EmblemsTest extends CardTestPlayerBase { assertHandCount(playerA, 0); } + + @Test + public void testJayaFieryNegotiator() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Jaya, Fiery Negotiator"); + addCard(Zone.HAND, playerA, "Wrenn's Resolve"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + addCard(Zone.LIBRARY, playerA, "Lightning Bolt", 2); + skipInitShuffling(); + + addCounters(1, PhaseStep.UPKEEP, playerA, "Jaya, Fiery Negotiator", CounterType.LOYALTY, 6); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-8: You get an emblem"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Wrenn's Resolve"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); + setChoice(playerA, false, 2); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertExileCount(playerA, 6 - 1); + assertLife(playerB, 20 - 3 * 3); + } } From 8b636b99267a3f8fe7470786b010237d158db8a9 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Sun, 17 Aug 2025 14:49:39 -0500 Subject: [PATCH 26/31] disable testNahiriSacrificePrevented test * will enable with #13916 --- .../org/mage/test/cards/single/one/NahirisSacrificeTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/one/NahirisSacrificeTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/one/NahirisSacrificeTest.java index 24087ff3541..e69179635a4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/one/NahirisSacrificeTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/one/NahirisSacrificeTest.java @@ -2,6 +2,7 @@ package org.mage.test.cards.single.one; import mage.constants.PhaseStep; import mage.constants.Zone; +import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -40,6 +41,7 @@ public class NahirisSacrificeTest extends CardTestPlayerBase { } @Test + @Ignore // Enable after merging #13916 public void testNahirisSacrificePrevented() { setStrictChooseMode(true); From 895c99d282c71f602082d16a189a617ce4ee5a63 Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Mon, 18 Aug 2025 12:08:06 -0400 Subject: [PATCH 27/31] [EOE] Consult the Star Charts needs LandsYouControlCount not ManaSpentToCastCount. --- Mage.Sets/src/mage/cards/c/ConsultTheStarCharts.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/c/ConsultTheStarCharts.java b/Mage.Sets/src/mage/cards/c/ConsultTheStarCharts.java index 6a2cfcbbe25..1363162cccc 100644 --- a/Mage.Sets/src/mage/cards/c/ConsultTheStarCharts.java +++ b/Mage.Sets/src/mage/cards/c/ConsultTheStarCharts.java @@ -2,7 +2,7 @@ package mage.cards.c; import mage.abilities.condition.common.KickedCondition; import mage.abilities.decorator.ConditionalOneShotEffect; -import mage.abilities.dynamicvalue.common.ManaSpentToCastCount; +import mage.abilities.dynamicvalue.common.LandsYouControlCount; import mage.abilities.effects.common.LookLibraryAndPickControllerEffect; import mage.abilities.keyword.KickerAbility; import mage.cards.CardImpl; @@ -25,8 +25,8 @@ public final class ConsultTheStarCharts extends CardImpl { // Look at the top X cards of your library, where X is the number of lands you control. Put one of those cards into your hand. If this spell was kicked, put two of those cards into your hand instead. Put the rest on the bottom of your library in a random order. this.getSpellAbility().addEffect(new ConditionalOneShotEffect( - new LookLibraryAndPickControllerEffect(ManaSpentToCastCount.instance, 2, PutCards.HAND, PutCards.BOTTOM_RANDOM), - new LookLibraryAndPickControllerEffect(ManaSpentToCastCount.instance, 1, PutCards.HAND, PutCards.BOTTOM_RANDOM), + new LookLibraryAndPickControllerEffect(LandsYouControlCount.instance, 2, PutCards.HAND, PutCards.BOTTOM_RANDOM), + new LookLibraryAndPickControllerEffect(LandsYouControlCount.instance, 1, PutCards.HAND, PutCards.BOTTOM_RANDOM), KickedCondition.ONCE, "look at the top X cards of your library, where X is the number of lands you control. " + "Put one of those cards into your hand. If this spell was kicked, put two of those cards into your hand instead. " + "Put the rest on the bottom of your library in a random order" From ca8b02d5ab22f1151bac79f15e681cdb62f8b2a1 Mon Sep 17 00:00:00 2001 From: jmlundeen Date: Tue, 19 Aug 2025 11:26:52 -0500 Subject: [PATCH 28/31] increase test coverage --- .../cards/single/dis/ExperimentKrajTest.java | 77 +++++++++++++++ .../cards/single/lci/EatenByPiranhasTest.java | 44 +++++++++ .../cards/single/neo/EaterOfVirtueTest.java | 54 +++++++++++ .../single/one/EncroachingMycosynthTest.java | 58 +++++++++++ .../cards/single/shm/ElsewhereFlaskTest.java | 57 +++++++++++ .../test/cards/single/tmp/ExcavatorTest.java | 54 +++++++++++ .../cards/single/who/EverybodyLivesTest.java | 95 +++++++++++++++++++ 7 files changed, 439 insertions(+) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/dis/ExperimentKrajTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/lci/EatenByPiranhasTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/neo/EaterOfVirtueTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/one/EncroachingMycosynthTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/shm/ElsewhereFlaskTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/ExcavatorTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/who/EverybodyLivesTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/dis/ExperimentKrajTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/dis/ExperimentKrajTest.java new file mode 100644 index 00000000000..2a0e364821b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/dis/ExperimentKrajTest.java @@ -0,0 +1,77 @@ +package org.mage.test.cards.single.dis; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.UntapAllControllerEffect; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import static org.junit.Assert.assertEquals; + +public class ExperimentKrajTest extends CardTestPlayerBase { + + /* + Experiment Kraj + {2}{G}{G}{U}{U} + Legendary Creature — Ooze Mutant + + Experiment Kraj has all activated abilities of each other creature with a +1/+1 counter on it. + + {T}: Put a +1/+1 counter on target creature. + */ + private static final String experimentKraj = "Experiment Kraj"; + /* + Stoneforge Mystic + {1}{W} + Creature — Kor Artificer + + When this creature enters, you may search your library for an Equipment card, reveal it, put it into your hand, then shuffle. + + {1}{W}, {T}: You may put an Equipment card from your hand onto the battlefield. + */ + private static final String stoneforgeMystic = "Stoneforge Mystic"; + /* + Noble Hierarch + {G} + Creature — Human Druid + + Exalted (Whenever a creature you control attacks alone, that creature gets +1/+1 until end of turn.) + + {T}: Add {G}, {W}, or {U}. + */ + private static final String nobleHierarch = "Noble Hierarch"; + + @Test + public void testExperimentKraj() { + setStrictChooseMode(true); + + Ability ability = new SimpleActivatedAbility( + Zone.ALL, + new UntapAllControllerEffect(StaticFilters.FILTER_CONTROLLED_A_CREATURE), + new ManaCostsImpl<>("") + ); + addCustomCardWithAbility("Untap creatures", playerA, ability); + + addCard(Zone.BATTLEFIELD, playerA, experimentKraj); + addCard(Zone.BATTLEFIELD, playerA, stoneforgeMystic); + addCard(Zone.BATTLEFIELD, playerB, nobleHierarch); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Put a +1/+1", stoneforgeMystic); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "untap all"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Put a +1/+1", nobleHierarch); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertEquals("Kraj should have 5 activated abilities", 5, getPermanent(experimentKraj).getAbilities(currentGame) + .stream() + .filter(Ability::isActivatedAbility) + .count()); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/EatenByPiranhasTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/EatenByPiranhasTest.java new file mode 100644 index 00000000000..55692a80fe0 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/lci/EatenByPiranhasTest.java @@ -0,0 +1,44 @@ +package org.mage.test.cards.single.lci; + +import mage.ObjectColor; +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +public class EatenByPiranhasTest extends CardTestPlayerBase { + + /* + Eaten by Piranhas + {1}{U} + Enchantment — Aura + + Flash (You may cast this spell any time you could cast an instant.) + + Enchant creature + + Enchanted creature loses all abilities and is a black Skeleton creature with base power and toughness 1/1. (It loses all other colors, card types, and creature types.) + */ + private static final String eatenByPiranhas = "Eaten by Piranhas"; + + @Test + public void testEatenByPiranhas() { + setStrictChooseMode(true); + addCard(Zone.HAND, playerB, eatenByPiranhas); + addCard(Zone.BATTLEFIELD, playerB, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, eatenByPiranhas); + addTarget(playerB, "Balduvian Bears"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertType("Balduvian Bears", CardType.CREATURE, SubType.SKELETON); + assertPowerToughness(playerA, "Balduvian Bears", 1, 1); + assertColor(playerA, "Balduvian Bears", ObjectColor.BLACK, true); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/neo/EaterOfVirtueTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/neo/EaterOfVirtueTest.java new file mode 100644 index 00000000000..6db049ab18b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/neo/EaterOfVirtueTest.java @@ -0,0 +1,54 @@ +package org.mage.test.cards.single.neo; + +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.LifelinkAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import java.util.Arrays; + + +public class EaterOfVirtueTest extends CardTestPlayerBase { + + /* + Eater of Virtue + {1} + Legendary Artifact — Equipment + + Whenever equipped creature dies, exile it. + + Equipped creature gets +2/+0. + + As long as a card exiled with Eater of Virtue has flying, equipped creature has flying. + The same is true for first strike, double strike, deathtouch, haste, hexproof, indestructible, lifelink, menace, protection, reach, trample, and vigilance. + + Equip {1} + */ + public static final String eaterOfVirtue = "Eater of Virtue"; + + @Test + public void testEaterOfVirtue() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, eaterOfVirtue); + addCard(Zone.BATTLEFIELD, playerA, "Island", 5); + addCard(Zone.BATTLEFIELD, playerA, "Adult Gold Dragon"); // Flying, Lifelink, Haste + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears"); + addCard(Zone.HAND, playerB, "Doom Blade"); + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Equip", "Adult Gold Dragon"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Doom Blade", "Adult Gold Dragon"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 1); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Equip", "Balduvian Bears"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertAbilities(playerA, "Balduvian Bears", Arrays.asList(FlyingAbility.getInstance(), LifelinkAbility.getInstance(), HasteAbility.getInstance())); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/one/EncroachingMycosynthTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/one/EncroachingMycosynthTest.java new file mode 100644 index 00000000000..8646657de6b --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/one/EncroachingMycosynthTest.java @@ -0,0 +1,58 @@ +package org.mage.test.cards.single.one; + +import mage.cards.Card; +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommanderDuelBase; + +import java.util.List; + +import static org.junit.Assert.assertTrue; + + +public class EncroachingMycosynthTest extends CardTestCommanderDuelBase { + + /* + Encroaching Mycosynth + {3}{U} + Artifact + + Nonland permanents you control are artifacts in addition to their other types. + The same is true for permanent spells you control and nonland permanent cards you own that aren’t on the battlefield. + */ + private static final String encroachingMycosynth = "Encroaching Mycosynth"; + private static final String balduvianBears = "Balduvian Bears"; + @Test + public void testEncroachingMycosynth() { + setStrictChooseMode(true); + + addCard(Zone.GRAVEYARD, playerA, balduvianBears); + addCard(Zone.HAND, playerA, balduvianBears, 2); + addCard(Zone.BATTLEFIELD, playerA, balduvianBears); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, encroachingMycosynth); + addCard(Zone.EXILED, playerA, balduvianBears); + addCard(Zone.LIBRARY, playerA, balduvianBears); + addCard(Zone.COMMAND, playerA, balduvianBears); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, balduvianBears); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertType(balduvianBears, CardType.ARTIFACT, true); + List cards = getHandCards(playerA); + cards.addAll(getLibraryCards(playerA)); + cards.addAll(getCommandCards(playerA)); + cards.addAll(getExiledCards(playerA)); + cards.addAll(getLibraryCards(playerA)); + for (Card card : cards) { + if (!card.isLand(currentGame)) { + assertTrue(card.getCardType(currentGame).contains(CardType.ARTIFACT)); + } + } + } + +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/shm/ElsewhereFlaskTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/shm/ElsewhereFlaskTest.java new file mode 100644 index 00000000000..40699057a95 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/shm/ElsewhereFlaskTest.java @@ -0,0 +1,57 @@ +package org.mage.test.cards.single.shm; + +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.SubType; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + + +public class ElsewhereFlaskTest extends CardTestPlayerBase { + + /* + Elsewhere Flask + {2} + Artifact + + When this artifact enters, draw a card. + + Sacrifice this artifact: Choose a basic land type. Each land you control becomes that type until end of turn. + */ + private static final String elsewhereFlask = "Elsewhere Flask"; + + @Test + public void testElsewhereFlask() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, elsewhereFlask); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Forest"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + setChoice(playerA, "Swamp"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertType("Island", CardType.LAND, SubType.SWAMP); + assertType("Forest", CardType.LAND, SubType.SWAMP); + } + + @Test + public void testElsewhereFlask2() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, elsewhereFlask); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Forest"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + setChoice(playerA, "Swamp"); + + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertType("Island", CardType.LAND, SubType.ISLAND); + assertType("Forest", CardType.LAND, SubType.FOREST); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/ExcavatorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/ExcavatorTest.java new file mode 100644 index 00000000000..7ee1daedf6e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tmp/ExcavatorTest.java @@ -0,0 +1,54 @@ +package org.mage.test.cards.single.tmp; + +import mage.abilities.keyword.LandwalkAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Ignore; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +public class ExcavatorTest extends CardTestPlayerBase { + + /* + Excavator + {2} + Artifact + + {T}, Sacrifice a basic land: Target creature gains landwalk of each of the land types of the sacrificed land until end of turn. + (It can’t be blocked as long as defending player controls a land of any of those types.) + */ + public static final String excavator = "Excavator"; + + /* + Leyline of the Guildpact + {G/W}{G/U}{B/G}{R/G} + Enchantment + + If this card is in your opening hand, you may begin the game with it on the battlefield. + + Each nonland permanent you control is all colors. + + Lands you control are every basic land type in addition to their other types. + */ + public static final String leylineOfTheGuildpact = "Leyline of the Guildpact"; + + @Test + @Ignore("Failing because permanent LKI does not save MageObjectAttribute values") + public void testExcavator() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, excavator); + addCard(Zone.BATTLEFIELD, playerA, leylineOfTheGuildpact); + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}"); + setChoice(playerA, "Island"); + addTarget(playerA, "Balduvian Bears"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertAbilityCount(playerA, "Balduvian Bears", LandwalkAbility.class, 5); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/who/EverybodyLivesTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/who/EverybodyLivesTest.java new file mode 100644 index 00000000000..c1cd4693912 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/who/EverybodyLivesTest.java @@ -0,0 +1,95 @@ +package org.mage.test.cards.single.who; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.LoseGameSourceControllerEffect; +import mage.abilities.effects.common.LoseLifeAllPlayersEffect; +import mage.abilities.effects.common.WinGameSourceControllerEffect; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + +public class EverybodyLivesTest extends CardTestCommander4Players { + + /* + Everybody Lives! + {1}{W} + Instant + + All creatures gain hexproof and indestructible until end of turn. Players gain hexproof until end of turn. + Players can’t lose life this turn and players can’t lose the game or win the game this turn. + */ + private static final String everybodyLives = "Everybody Lives!"; + + @Test + public void testEverybodyLivesCantLoseLifeAndHexproof() { + setStrictChooseMode(true); + + addCustomCardWithAbility("lose life effect", playerA, new SimpleActivatedAbility( + new LoseLifeAllPlayersEffect(20), + new ManaCostsImpl<>("")) + ); + addCard(Zone.HAND, playerA, "Lightning Bolt"); + addCard(Zone.HAND, playerA, everybodyLives); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, everybodyLives, true); + checkPlayableAbility("Can't cast lightning bolt", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "each player"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + assertLife(playerC, 20); + assertLife(playerD, 20); + } + + @Test + public void testEverybodyLivesCantLoseGame() { + setStrictChooseMode(true); + + addCustomCardWithAbility("lose game effect", playerA, new SimpleActivatedAbility( + new LoseGameSourceControllerEffect(), + new ManaCostsImpl<>("")) + ); + addCard(Zone.HAND, playerA, everybodyLives); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, everybodyLives, true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "you lose"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertHasNotLostTheGame(playerA); + + } + + @Test + public void testEverybodyLivesCantWinGame() { + setStrictChooseMode(true); + + addCustomCardWithAbility("win game effect", playerA, new SimpleActivatedAbility( + new WinGameSourceControllerEffect(), + new ManaCostsImpl<>("")) + ); + addCard(Zone.HAND, playerA, everybodyLives); + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.BATTLEFIELD, playerA, "Plains"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, everybodyLives, true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "you win"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertHasNotWonTheGame(playerA); + + } + +} From dd67682c12ceaefaf2679b8b0e78a51c15a03e3c Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:15:29 -0400 Subject: [PATCH 29/31] Make Tree of Perdition's effect check to make sure that it's a creature before swapping toughness, because noncreatures do not have toughness. --- Mage.Sets/src/mage/cards/t/TreeOfPerdition.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/t/TreeOfPerdition.java b/Mage.Sets/src/mage/cards/t/TreeOfPerdition.java index 07de60bce02..d3d755cda2d 100644 --- a/Mage.Sets/src/mage/cards/t/TreeOfPerdition.java +++ b/Mage.Sets/src/mage/cards/t/TreeOfPerdition.java @@ -63,7 +63,7 @@ class TreeOfPerditionEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player opponent = game.getPlayer(source.getFirstTarget()); Permanent perm = game.getPermanent(source.getSourceId()); - if (perm == null || opponent == null || !opponent.isLifeTotalCanChange()) { + if (perm == null || opponent == null || !opponent.isLifeTotalCanChange() || !perm.isCreature()) { return false; } From f3d685596541e8399514a3b8b3d1266090d8c79e Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:17:56 -0400 Subject: [PATCH 30/31] Fix the other two effects which exchange life totals with power/toughness to check to make sure that the permanent is a creature. --- Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java | 2 +- Mage.Sets/src/mage/cards/t/TreeOfRedemption.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java b/Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java index 09ef0580bc4..4a77cb0874d 100644 --- a/Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java +++ b/Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java @@ -64,7 +64,7 @@ class EvraHalcyonWitnessEffect extends OneShotEffect { Player player = game.getPlayer(source.getControllerId()); if (player != null && player.isLifeTotalCanChange()) { Permanent perm = game.getPermanent(source.getSourceId()); - if (perm != null) { + if (perm != null && perm.isCreature()) { int amount = perm.getPower().getValue(); int life = player.getLife(); if (life == amount) { diff --git a/Mage.Sets/src/mage/cards/t/TreeOfRedemption.java b/Mage.Sets/src/mage/cards/t/TreeOfRedemption.java index 25a6806d764..9d29e2dd38f 100644 --- a/Mage.Sets/src/mage/cards/t/TreeOfRedemption.java +++ b/Mage.Sets/src/mage/cards/t/TreeOfRedemption.java @@ -59,7 +59,7 @@ class TreeOfRedemptionEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); Permanent perm = game.getPermanent(source.getSourceId()); - if (perm == null || player == null || !player.isLifeTotalCanChange()) { + if (perm == null || player == null || !player.isLifeTotalCanChange() || !perm.isCreature()) { return false; } From b085446d04ee2eaf23c912bb18a7328f977368fb Mon Sep 17 00:00:00 2001 From: Grath <1895280+Grath@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:22:29 -0400 Subject: [PATCH 31/31] Include game parameter. --- Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java | 2 +- Mage.Sets/src/mage/cards/t/TreeOfPerdition.java | 2 +- Mage.Sets/src/mage/cards/t/TreeOfRedemption.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java b/Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java index 4a77cb0874d..bce8344342e 100644 --- a/Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java +++ b/Mage.Sets/src/mage/cards/e/EvraHalcyonWitness.java @@ -64,7 +64,7 @@ class EvraHalcyonWitnessEffect extends OneShotEffect { Player player = game.getPlayer(source.getControllerId()); if (player != null && player.isLifeTotalCanChange()) { Permanent perm = game.getPermanent(source.getSourceId()); - if (perm != null && perm.isCreature()) { + if (perm != null && perm.isCreature(game)) { int amount = perm.getPower().getValue(); int life = player.getLife(); if (life == amount) { diff --git a/Mage.Sets/src/mage/cards/t/TreeOfPerdition.java b/Mage.Sets/src/mage/cards/t/TreeOfPerdition.java index d3d755cda2d..6c7ac347f8c 100644 --- a/Mage.Sets/src/mage/cards/t/TreeOfPerdition.java +++ b/Mage.Sets/src/mage/cards/t/TreeOfPerdition.java @@ -63,7 +63,7 @@ class TreeOfPerditionEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player opponent = game.getPlayer(source.getFirstTarget()); Permanent perm = game.getPermanent(source.getSourceId()); - if (perm == null || opponent == null || !opponent.isLifeTotalCanChange() || !perm.isCreature()) { + if (perm == null || opponent == null || !opponent.isLifeTotalCanChange() || !perm.isCreature(game)) { return false; } diff --git a/Mage.Sets/src/mage/cards/t/TreeOfRedemption.java b/Mage.Sets/src/mage/cards/t/TreeOfRedemption.java index 9d29e2dd38f..9e8cb4c558f 100644 --- a/Mage.Sets/src/mage/cards/t/TreeOfRedemption.java +++ b/Mage.Sets/src/mage/cards/t/TreeOfRedemption.java @@ -59,7 +59,7 @@ class TreeOfRedemptionEffect extends OneShotEffect { public boolean apply(Game game, Ability source) { Player player = game.getPlayer(source.getControllerId()); Permanent perm = game.getPermanent(source.getSourceId()); - if (perm == null || player == null || !player.isLifeTotalCanChange() || !perm.isCreature()) { + if (perm == null || player == null || !player.isLifeTotalCanChange() || !perm.isCreature(game)) { return false; }