From 0352d84ca59847604c7637eaaec5c916ba5af80a Mon Sep 17 00:00:00 2001 From: balazskristof <20043803+balazskristof@users.noreply.github.com> Date: Thu, 15 May 2025 08:21:31 +0200 Subject: [PATCH 1/2] [M3C] Implement Desert Warfare --- Mage.Sets/src/mage/cards/d/DesertWarfare.java | 153 ++++++++++++++++++ .../mage/sets/ModernHorizons3Commander.java | 1 + 2 files changed, 154 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/d/DesertWarfare.java diff --git a/Mage.Sets/src/mage/cards/d/DesertWarfare.java b/Mage.Sets/src/mage/cards/d/DesertWarfare.java new file mode 100644 index 00000000000..f60f9dc3706 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DesertWarfare.java @@ -0,0 +1,153 @@ +package mage.cards.d; + +import java.util.UUID; + +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; +import mage.abilities.common.delayed.AtTheBeginOfPlayersNextEndStepDelayedTriggeredAbility; +import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; +import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; +import mage.abilities.effects.Effect; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.ReturnToBattlefieldUnderYourControlTargetEffect; +import mage.abilities.effects.common.continuous.GainAbilityTargetEffect; +import mage.abilities.hint.ValueHint; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.triggers.BeginningOfCombatTriggeredAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.filter.common.FilterControlledPermanent; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.SacrificedPermanentEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.SandWarriorToken; +import mage.game.permanent.token.Token; +import mage.target.targetpointer.FixedTarget; +import mage.target.targetpointer.FixedTargets; + +/** + * @author balazskristof + */ +public final class DesertWarfare extends CardImpl { + + private static final FilterControlledPermanent filter = new FilterControlledPermanent( + SubType.DESERT, "you control five or more deserts" + ); + + public DesertWarfare(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{3}{G}"); + + // Whenever you sacrifice a Desert and whenever a Desert card is put into your graveyard from your hand or library, put that card onto the battlefield under your control at the beginning of your next end step. + this.addAbility(new DesertWarfareTriggeredAbility()); + + // At the beginning of combat on your turn, if you control five or more Deserts, create that many 1/1 red, green, and white Sand Warrior creature tokens. They gain haste. + this.addAbility(new ConditionalInterveningIfTriggeredAbility( + new BeginningOfCombatTriggeredAbility(new DesertWarfareTokenEffect()), + new PermanentsOnTheBattlefieldCondition(filter, ComparisonType.OR_GREATER, 5), + "At the beginning of combat on your turn, if you control five or more Deserts, " + + "create that many 1/1 red, green, and white Sand Warrior creature tokens. They gain haste." + ).addHint(new ValueHint("Deserts you control", new PermanentsOnBattlefieldCount(filter)))); + } + + private DesertWarfare(final DesertWarfare card) { + super(card); + } + + @Override + public DesertWarfare copy() { + return new DesertWarfare(this); + } +} + +class DesertWarfareTriggeredAbility extends TriggeredAbilityImpl { + + DesertWarfareTriggeredAbility() { + super(Zone.BATTLEFIELD, null, false); + setTriggerPhrase("Whenever you sacrifice a Desert and whenever a Desert card is put into your graveyard from your hand or library, " + + "put that card onto the battlefield under your control at the beginning of your next end step."); + } + + private DesertWarfareTriggeredAbility(final DesertWarfareTriggeredAbility ability) { + super(ability); + } + + @Override + public DesertWarfareTriggeredAbility copy() { + return new DesertWarfareTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SACRIFICED_PERMANENT + || event.getType() == GameEvent.EventType.ZONE_CHANGE; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + if (!(event instanceof ZoneChangeEvent) && !(event instanceof SacrificedPermanentEvent)) { + return false; + } + Effect effect = new ReturnToBattlefieldUnderYourControlTargetEffect(); + if (event instanceof ZoneChangeEvent) { + ZoneChangeEvent zce = (ZoneChangeEvent) event; + Card card = game.getCard(event.getTargetId()); + if (card == null + || !card.isOwnedBy(getControllerId()) + || !card.hasSubtype(SubType.DESERT, game) + || zce.getToZone() != Zone.GRAVEYARD + || (zce.getFromZone() != Zone.HAND + && zce.getFromZone() != Zone.LIBRARY)) { + return false; + } + effect.setTargetPointer(new FixedTarget(card, game)); + } else { + Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); + if (permanent == null + || !permanent.isOwnedBy(getControllerId()) + || !permanent.hasSubtype(SubType.DESERT, game)) { + return false; + } + effect.setTargetPointer(new FixedTarget(permanent, game)); + } + game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect, TargetController.YOU), this); + return true; + } +} + +class DesertWarfareTokenEffect extends OneShotEffect { + + private static final FilterControlledPermanent filter = new FilterControlledPermanent(SubType.DESERT); + + DesertWarfareTokenEffect() { + super(Outcome.Benefit); + staticText = "create that many 1/1 red, green, and white Sand Warrior creature tokens. " + + " They gain haste."; + } + + private DesertWarfareTokenEffect(final DesertWarfareTokenEffect effect) { + super(effect); + } + + @Override + public DesertWarfareTokenEffect copy() { + return new DesertWarfareTokenEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Token token = new SandWarriorToken(); + int count = game.getBattlefield().getActivePermanents(filter, source.getControllerId(), source, game).size(); + token.putOntoBattlefield(count, game, source, source.getControllerId()); + game.addEffect(new GainAbilityTargetEffect( + HasteAbility.getInstance(), Duration.WhileOnBattlefield + ).setTargetPointer(new FixedTargets(token, game)), source); + return true; + } +} diff --git a/Mage.Sets/src/mage/sets/ModernHorizons3Commander.java b/Mage.Sets/src/mage/sets/ModernHorizons3Commander.java index 75b160e0e42..659c385ab9f 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons3Commander.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons3Commander.java @@ -96,6 +96,7 @@ public final class ModernHorizons3Commander extends ExpansionSet { cards.add(new SetCardInfo("Decoction Module", 288, Rarity.UNCOMMON, mage.cards.d.DecoctionModule.class)); cards.add(new SetCardInfo("Deepfathom Skulker", 180, Rarity.RARE, mage.cards.d.DeepfathomSkulker.class)); cards.add(new SetCardInfo("Demolition Field", 335, Rarity.UNCOMMON, mage.cards.d.DemolitionField.class)); + cards.add(new SetCardInfo("Desert Warfare", 64, Rarity.RARE, mage.cards.d.DesertWarfare.class)); cards.add(new SetCardInfo("Desert of the Indomitable", 336, Rarity.COMMON, mage.cards.d.DesertOfTheIndomitable.class)); cards.add(new SetCardInfo("Desert of the Mindful", 337, Rarity.COMMON, mage.cards.d.DesertOfTheMindful.class)); cards.add(new SetCardInfo("Disa the Restless", 1, Rarity.MYTHIC, mage.cards.d.DisaTheRestless.class)); From 1b80010e68167d19f7fba07fc834ec26d866054a Mon Sep 17 00:00:00 2001 From: balazskristof <20043803+balazskristof@users.noreply.github.com> Date: Sat, 17 May 2025 14:48:15 +0200 Subject: [PATCH 2/2] [M3C] Desert Warfare tests --- Mage.Sets/src/mage/cards/d/DesertWarfare.java | 11 +- .../cards/single/m3c/DesertWarfareTest.java | 276 ++++++++++++++++++ 2 files changed, 278 insertions(+), 9 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/DesertWarfareTest.java diff --git a/Mage.Sets/src/mage/cards/d/DesertWarfare.java b/Mage.Sets/src/mage/cards/d/DesertWarfare.java index f60f9dc3706..68bea605cf9 100644 --- a/Mage.Sets/src/mage/cards/d/DesertWarfare.java +++ b/Mage.Sets/src/mage/cards/d/DesertWarfare.java @@ -5,10 +5,8 @@ import java.util.UUID; import mage.abilities.Ability; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; -import mage.abilities.common.delayed.AtTheBeginOfPlayersNextEndStepDelayedTriggeredAbility; import mage.abilities.condition.common.PermanentsOnTheBattlefieldCondition; import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility; -import mage.abilities.decorator.ConditionalOneShotEffect; import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; @@ -24,7 +22,6 @@ import mage.constants.*; import mage.filter.common.FilterControlledPermanent; import mage.game.Game; import mage.game.events.GameEvent; -import mage.game.events.SacrificedPermanentEvent; import mage.game.events.ZoneChangeEvent; import mage.game.permanent.Permanent; import mage.game.permanent.token.SandWarriorToken; @@ -91,10 +88,6 @@ class DesertWarfareTriggeredAbility extends TriggeredAbilityImpl { @Override public boolean checkTrigger(GameEvent event, Game game) { - if (!(event instanceof ZoneChangeEvent) && !(event instanceof SacrificedPermanentEvent)) { - return false; - } - Effect effect = new ReturnToBattlefieldUnderYourControlTargetEffect(); if (event instanceof ZoneChangeEvent) { ZoneChangeEvent zce = (ZoneChangeEvent) event; Card card = game.getCard(event.getTargetId()); @@ -106,7 +99,6 @@ class DesertWarfareTriggeredAbility extends TriggeredAbilityImpl { && zce.getFromZone() != Zone.LIBRARY)) { return false; } - effect.setTargetPointer(new FixedTarget(card, game)); } else { Permanent permanent = game.getPermanentOrLKIBattlefield(event.getTargetId()); if (permanent == null @@ -114,8 +106,9 @@ class DesertWarfareTriggeredAbility extends TriggeredAbilityImpl { || !permanent.hasSubtype(SubType.DESERT, game)) { return false; } - effect.setTargetPointer(new FixedTarget(permanent, game)); } + Effect effect = new ReturnToBattlefieldUnderYourControlTargetEffect(); + effect.setTargetPointer(new FixedTarget(event.getTargetId(), game)); game.addDelayedTriggeredAbility(new AtTheBeginOfNextEndStepDelayedTriggeredAbility(effect, TargetController.YOU), this); return true; } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/DesertWarfareTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/DesertWarfareTest.java new file mode 100644 index 00000000000..06b9dde6760 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/m3c/DesertWarfareTest.java @@ -0,0 +1,276 @@ +package org.mage.test.cards.single.m3c; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author balazskristof + */ +public class DesertWarfareTest extends CardTestPlayerBase { + + @Test + public void TestTokens() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + addCard(Zone.BATTLEFIELD, playerA, "Desert", 4); + addCard(Zone.HAND, playerA, "Desert"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertTokenCount(playerA, "Sand Warrior Token", 0); + + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + assertTokenCount(playerA, "Sand Warrior Token", 0); + + playLand(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Desert"); + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + assertTokenCount(playerA, "Sand Warrior Token", 5); + + setStopAt(4, PhaseStep.BEGIN_COMBAT); + execute(); + assertTokenCount(playerA, "Sand Warrior Token", 5); + + setStopAt(5, PhaseStep.BEGIN_COMBAT); + execute(); + assertTokenCount(playerA, "Sand Warrior Token", 10); + } + + @Test + public void TestSacrifice() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + + addCard(Zone.BATTLEFIELD, playerA, "Desert"); + addCard(Zone.BATTLEFIELD, playerA, "Akki Avalanchers"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + setChoice(playerA, "Desert"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Desert", 1); + assertPermanentCount(playerA, "Desert", 0); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert", 0); + assertPermanentCount(playerA, "Desert", 1); + } + + @Test + public void TestSacrificeOpponentsTurn() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + + addCard(Zone.BATTLEFIELD, playerA, "Desert"); + addCard(Zone.BATTLEFIELD, playerA, "Akki Avalanchers"); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Sacrifice"); + setChoice(playerA, "Desert"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert", 1); + assertPermanentCount(playerA, "Desert", 0); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert", 0); + assertPermanentCount(playerA, "Desert", 1); + } + + @Test + public void TestDestroy() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + addCard(Zone.BATTLEFIELD, playerA, "Desert"); + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 4); + addCard(Zone.HAND, playerB, "Volcanic Upheaval"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Volcanic Upheaval"); + addTarget(playerB, "Desert"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert", 1); + assertPermanentCount(playerA, "Desert", 0); + } + + @Test + public void TestDiscard() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Island"); + addCard(Zone.LIBRARY, playerA, "Desert"); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Faithless Looting"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Faithless Looting"); + setChoice(playerA, "Island^Desert"); + //setChoice(playerA, "Desert"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert", 0); + assertGraveyardCount(playerA, "Island", 1); + assertPermanentCount(playerA, "Desert", 1); + } + + @Test + public void TestDiscardOpponentsTurn() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Island"); + addCard(Zone.LIBRARY, playerA, "Desert"); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain"); + addCard(Zone.HAND, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Faithless Looting"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Faithless Looting"); + setChoice(playerA, "Island"); + setChoice(playerA, "Desert"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertGraveyardCount(playerA, "Desert", 0); + assertGraveyardCount(playerA, "Island", 1); + assertPermanentCount(playerA, "Desert", 1); + } + + @Test + public void TestCycling() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Desert of the Fervent"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cycling {1}{R}"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert of the Fervent", 0); + assertPermanentCount(playerA, "Desert of the Fervent", 1); + } + + @Test + public void TestCyclingOpponentsTurn() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Desert of the Fervent"); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cycling {1}{R}"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert of the Fervent", 1); + assertPermanentCount(playerA, "Desert of the Fervent", 0); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert of the Fervent", 0); + assertPermanentCount(playerA, "Desert of the Fervent", 1); + } + + @Test + public void TestMill() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Plains"); + addCard(Zone.LIBRARY, playerA, "Island"); + addCard(Zone.LIBRARY, playerA, "Desert"); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.HAND, playerA, "Stitcher's Supplier"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stitcher's Supplier"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert", 0); + assertPermanentCount(playerA, "Desert", 1); + assertGraveyardCount(playerA, "Plains", 1); + assertGraveyardCount(playerA, "Island", 1); + } + + @Test + public void TestMillOpponentsTurn() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Plains"); + addCard(Zone.LIBRARY, playerA, "Island"); + addCard(Zone.LIBRARY, playerA, "Desert"); + + addCard(Zone.BATTLEFIELD, playerA, "Stitcher's Supplier"); + + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + addCard(Zone.HAND, playerB, "Shock"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Shock"); + addTarget(playerB, "Stitcher's Supplier"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Desert", 0); + assertGraveyardCount(playerA, "Desert", 1); + assertGraveyardCount(playerA, "Plains", 1); + assertGraveyardCount(playerA, "Island", 1); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, "Desert", 1); + assertGraveyardCount(playerA, "Desert", 0); + assertGraveyardCount(playerA, "Plains", 1); + assertGraveyardCount(playerA, "Island", 1); + } + + @Test + public void TestDuneChanter() { + addCard(Zone.BATTLEFIELD, playerA, "Desert Warfare"); + addCard(Zone.BATTLEFIELD, playerA, "Dune Chanter"); + + skipInitShuffling(); + addCard(Zone.LIBRARY, playerA, "Plains"); + addCard(Zone.LIBRARY, playerA, "Island"); + addCard(Zone.LIBRARY, playerA, "Desert"); + + addCard(Zone.BATTLEFIELD, playerA, "Swamp"); + addCard(Zone.HAND, playerA, "Stitcher's Supplier"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Stitcher's Supplier"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerA, "Desert", 0); + assertGraveyardCount(playerA, "Plains", 0); + assertGraveyardCount(playerA, "Island", 0); + assertPermanentCount(playerA, "Desert", 1); + assertPermanentCount(playerA, "Plains", 1); + assertPermanentCount(playerA, "Island", 1); + + } +} +