From e92031a3bbcf92204dda0b5f7de087697fd4c261 Mon Sep 17 00:00:00 2001 From: Codermann63 <96649204+Codermann63@users.noreply.github.com> Date: Thu, 14 Sep 2023 03:12:09 +0200 Subject: [PATCH] [WOC] Implement Unfinished Business (#11144) * [WOC] Implement Unfinished Business * Added assertIsNotAttachedTo * Added unittests for [WOC] Unfinished Business * Remove unused import * Refactored assertIsAttachedTo & assertIsNotAttachedTo into assertIsAttached. * Added processAction after return of creature * Added comments and minor changes --------- Co-authored-by: Codermann63 --- .../src/mage/cards/u/UnfinishedBusiness.java | 126 ++++++++++++++++ .../mage/sets/WildsOfEldraineCommander.java | 1 + .../abilities/activated/ReconfigureTest.java | 6 +- .../abilities/enters/BronzehideLionTest.java | 2 +- .../single/afc/BeltOfGiantStrengthTest.java | 4 +- .../test/cards/single/ltr/ForgeAnewTest.java | 2 +- .../single/woc/UnfinishedBusinessTest.java | 138 ++++++++++++++++++ .../base/impl/CardTestPlayerAPIImpl.java | 15 +- 8 files changed, 281 insertions(+), 13 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/u/UnfinishedBusiness.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/woc/UnfinishedBusinessTest.java diff --git a/Mage.Sets/src/mage/cards/u/UnfinishedBusiness.java b/Mage.Sets/src/mage/cards/u/UnfinishedBusiness.java new file mode 100644 index 00000000000..e3b52374ac8 --- /dev/null +++ b/Mage.Sets/src/mage/cards/u/UnfinishedBusiness.java @@ -0,0 +1,126 @@ +package mage.cards.u; + +import java.util.UUID; + + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.cards.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.FilterCard; +import mage.filter.StaticFilters; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCardInYourGraveyard; + +/** + * + * @author Codermann63 + */ +public final class UnfinishedBusiness extends CardImpl { + + private static final FilterCard auraOrEquipmentFilter = new FilterCard("Aura or Equipment card"); + + static { + auraOrEquipmentFilter.add(Predicates.or( + SubType.EQUIPMENT.getPredicate(), + SubType.AURA.getPredicate() + )); + } + + public UnfinishedBusiness(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{W}{W}"); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(1,1, StaticFilters.FILTER_CARD_CREATURE)); + this.getSpellAbility().addTarget(new TargetCardInYourGraveyard(0, 2, auraOrEquipmentFilter)); + // Return target creature card from your graveyard to the battlefield, + // then return up to two target Aura and/or Equipment cards from your graveyard to the battlefield attached to that creature. + this.getSpellAbility().addEffect(new UnfinishedBusinessEffect()); + } + + private UnfinishedBusiness(final UnfinishedBusiness card) { + super(card); + } + + @Override + public UnfinishedBusiness copy() { + return new UnfinishedBusiness(this); + } +} + +class UnfinishedBusinessEffect extends OneShotEffect{ + + UnfinishedBusinessEffect() { + super(Outcome.PutCreatureInPlay); + staticText = "Return target creature card from your graveyard to the battlefield, then return up to two target Aura and/or Equipment cards from your graveyard to the battlefield attached to that creature. (If the Auras can not enchant that creature, they remain in your graveyard.)"; + } + + private UnfinishedBusinessEffect(final UnfinishedBusinessEffect effect) {super(effect);} + + @Override + public boolean apply(Game game, Ability source) { + Player controller = game.getPlayer((source.getControllerId())); + if (controller == null){ + return false; + } + + // Return target creature from the graveyard to the battlefield + Card targetCreature = game.getCard(source.getTargets().getFirstTarget()); + + if (targetCreature != null){ + controller.moveCards(targetCreature, Zone.BATTLEFIELD, source, game); + game.getState().processAction(game); + } + Permanent permanentCreature = targetCreature == null ? null : game.getPermanent(targetCreature.getId()); + + // Target auras and/or equipment in your graveyard. + Cards cardsInitial = new CardsImpl(source.getTargets().get(1).getTargets()); + if (cardsInitial.isEmpty()) { + return false; + } + + // Auras that cannot be attached to the creature stay in the graveyard + // Create a list of legal cards to return + Cards cards = new CardsImpl(); + for(UUID c: cardsInitial){ + if (game.getCard(c).hasSubtype(SubType.EQUIPMENT,game)){ + // always add equipment cards + cards.add(c); + } + else if (permanentCreature != null && + !permanentCreature.cantBeAttachedBy(game.getCard(c),source, game, false) && + game.getCard(c).hasSubtype(SubType.AURA, game)){ + // only add auras if the creature has returned + // only add auras that can be attached to creature + cards.add(c); + } + } + if (cards.isEmpty()){ + return false; + } + + // Handle return of legal auras and equipment + if (permanentCreature != null){ + cards.getCards(game) + .forEach(card -> game.getState().setValue("attachTo:" + card.getId(), permanentCreature)); + } + controller.moveCards(cards, Zone.BATTLEFIELD, source, game); + if (permanentCreature != null){ + for(UUID id: cards){ + if (!permanentCreature.cantBeAttachedBy(game.getCard(id), source, game, true)){ + permanentCreature.addAttachment(id, source, game); + } + } + } + return true; + } + + @Override + public UnfinishedBusinessEffect copy() { + return new UnfinishedBusinessEffect(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java b/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java index c23b03bdb3e..628656e3e6f 100644 --- a/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java +++ b/Mage.Sets/src/mage/sets/WildsOfEldraineCommander.java @@ -148,6 +148,7 @@ public final class WildsOfEldraineCommander extends ExpansionSet { cards.add(new SetCardInfo("Tithe Taker", 80, Rarity.RARE, mage.cards.t.TitheTaker.class)); cards.add(new SetCardInfo("Transcendent Envoy", 81, Rarity.COMMON, mage.cards.t.TranscendentEnvoy.class)); cards.add(new SetCardInfo("Umbra Mystic", 82, Rarity.RARE, mage.cards.u.UmbraMystic.class)); + cards.add(new SetCardInfo("Unfinished Business", 8, Rarity.RARE, mage.cards.u.UnfinishedBusiness.class)); cards.add(new SetCardInfo("Utopia Sprawl", 135, Rarity.COMMON, mage.cards.u.UtopiaSprawl.class)); cards.add(new SetCardInfo("Verdant Embrace", 136, Rarity.RARE, mage.cards.v.VerdantEmbrace.class)); cards.add(new SetCardInfo("Vitu-Ghazi, the City-Tree", 173, Rarity.UNCOMMON, mage.cards.v.VituGhaziTheCityTree.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/activated/ReconfigureTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/activated/ReconfigureTest.java index 0822fd180c8..73ea0307797 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/activated/ReconfigureTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/activated/ReconfigureTest.java @@ -33,7 +33,7 @@ public class ReconfigureTest extends CardTestPlayerBase { assertType(boar, CardType.CREATURE, false); assertSubtype(boar, SubType.EQUIPMENT); - assertIsAttachedTo(playerA, boar, lion); + assertAttachedTo(playerA, boar, lion, true); assertPowerToughness(playerA, lion, 2 + 3, 2 + 2); assertAbility(playerA, lion, TrampleAbility.getInstance(), true); } @@ -74,7 +74,7 @@ public class ReconfigureTest extends CardTestPlayerBase { assertType(boar, CardType.CREATURE, false); assertSubtype(boar, SubType.EQUIPMENT); - assertIsAttachedTo(playerA, boar, lion); + assertAttachedTo(playerA, boar, lion, true); assertPowerToughness(playerA, lion, 2 + 3, 2 + 2); assertAbility(playerA, lion, TrampleAbility.getInstance(), true); } @@ -94,7 +94,7 @@ public class ReconfigureTest extends CardTestPlayerBase { assertType(boar, CardType.CREATURE, false); assertSubtype(boar, SubType.EQUIPMENT); - assertIsAttachedTo(playerA, boar, lion); + assertAttachedTo(playerA, boar, lion, true); assertPowerToughness(playerA, lion, 2 + 3, 2 + 2); assertAbility(playerA, lion, TrampleAbility.getInstance(), true); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/BronzehideLionTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/BronzehideLionTest.java index 1ce2a16fe26..ad92bd91359 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/BronzehideLionTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/enters/BronzehideLionTest.java @@ -27,7 +27,7 @@ public class BronzehideLionTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); assertGraveyardCount(playerA, lion, 0); - assertIsAttachedTo(playerA, lion, "Grizzly Bears"); + assertAttachedTo(playerA, lion, "Grizzly Bears", true); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java index 166afca8762..f79679ec443 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/BeltOfGiantStrengthTest.java @@ -28,7 +28,7 @@ public class BeltOfGiantStrengthTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); - assertIsAttachedTo(playerA, belt, gigantosauras); + assertAttachedTo(playerA, belt, gigantosauras, true); Assert.assertTrue( "All Forests should be untapped", currentGame @@ -51,6 +51,6 @@ public class BeltOfGiantStrengthTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); - assertIsAttachedTo(playerA, belt, gigantosauras); + assertAttachedTo(playerA, belt, gigantosauras, true); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/ForgeAnewTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/ForgeAnewTest.java index d19dd55424a..dc5770dfe35 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/ForgeAnewTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/ltr/ForgeAnewTest.java @@ -39,6 +39,6 @@ public class ForgeAnewTest extends CardTestPlayerBase { execute(); // Make sure it is attached - assertIsAttachedTo(playerA, EQUIPMENT, CREATURE); + assertAttachedTo(playerA, EQUIPMENT, CREATURE, true); } } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/woc/UnfinishedBusinessTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/woc/UnfinishedBusinessTest.java new file mode 100644 index 00000000000..0a6684a6bd3 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/woc/UnfinishedBusinessTest.java @@ -0,0 +1,138 @@ +package org.mage.test.cards.single.woc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Codermann63 + */ + +public class UnfinishedBusinessTest extends CardTestPlayerBase { + /* + * Unfinished Business + * {3}{W}{W} - Sorcery + * Return target creature card from your graveyard to the battlefield, + * then return up to two target Aura and/or Equipment cards from your graveyard to the battlefield attached to that creature. + * (If the Auras can’t enchant that creature, they remain in your graveyard.) + */ + private static final String UNFINISHEDBUSINESS = "Unfinished Business"; + // Deadly insect - 6/1 creature with Shroud + private static final String SHROUDCREATURE = "Deadly Insect"; + // Apostle of Purifying Light - white creature with protection from black & activated ability to exile target card from a graveyard. + private static final String APOSTLE = "Apostle of Purifying Light"; + private static final String BEAR = "Grizzly Bears"; + // Blanchwood Armor - green aura - enchanted creature gets +1/+1 for each forest you control + private static final String AURA = "Blanchwood Armor"; + // Ghoulflesh - black aura - enchanted creature get -1/-1 and is a black Zombie in addition to its other colors and types. + private static final String GHOULFLESH = "Ghoulflesh"; + // Shuko - colorless Equipment - Equipped creature gets +1/+0. + private static final String EQUIPMENT = "Shuko"; + // Enormous Energy Blade - black equipment - Equipped creature gets +4/+0. Whenever Enormous Energy Blade becomes attached to a creature, tap that creature. + private static final String EEB = "Enormous Energy Blade"; + + + // Return a creature with shroud, and return an Aura and an Equipment checking that both attach. + @Test + public void testShroud() { + addCard(Zone.HAND, playerA, UNFINISHEDBUSINESS); + addCard(Zone.GRAVEYARD, playerA, SHROUDCREATURE, 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.GRAVEYARD, playerA, AURA); + addCard(Zone.GRAVEYARD, playerA, EQUIPMENT); + + setStrictChooseMode(true); + + castSpell(1,PhaseStep.PRECOMBAT_MAIN,playerA,UNFINISHEDBUSINESS,SHROUDCREATURE); + addTarget(playerA, AURA+"^"+EQUIPMENT); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + execute(); + + // Check that all returned to the battlefield + assertPermanentCount(playerA, SHROUDCREATURE, 1); + assertPermanentCount(playerA, AURA, 1); + assertPermanentCount(playerA, EQUIPMENT, 1); + + // Check aura and equipment is attached + assertAttachedTo(playerA, AURA, SHROUDCREATURE, true); + assertAttachedTo(playerA, EQUIPMENT, SHROUDCREATURE, true); + } + + + + // Return Apostle of purifying light (creature with protection from black), + // and try to return a Ghoulflesh(black aura) and Enormous energy blade(black equipment). + // The aura should remain in graveyard and the equipment should return but not attach. + @Test + public void testProtection(){ + addCard(Zone.HAND, playerA, UNFINISHEDBUSINESS); + // Nexus wardens gain life if an enchantment entered the battlefield + // This is to test if ghoulflesh enters the battlefield + addCard(Zone.BATTLEFIELD, playerA, "Nexus Wardens"); + addCard(Zone.GRAVEYARD, playerA, APOSTLE); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.GRAVEYARD, playerA, GHOULFLESH); + addCard(Zone.GRAVEYARD, playerA, EEB); + + setStrictChooseMode(true); + + castSpell(1,PhaseStep.PRECOMBAT_MAIN,playerA,UNFINISHEDBUSINESS,APOSTLE); + addTarget(playerA, GHOULFLESH+"^"+EEB); + waitStackResolved(1, PhaseStep.END_TURN); + + execute(); + + // Check boardstate + assertPermanentCount(playerA, APOSTLE, 1); + assertPermanentCount(playerA, GHOULFLESH, 0); + assertPermanentCount(playerA, EEB, 1); + + // EEB should never have been attached and therefore the White knight should be untapped + assertTapped(APOSTLE,false); + assertAttachedTo(playerA, EEB, APOSTLE,false); + + // Check that Ghoulflesh never entered the battlefield + assertLife(playerA, 20); + assertGraveyardCount(playerA,GHOULFLESH, 1); + } + + + + // Test equipment return if creature is exiled from graveyard before spell resolution + @Test + public void testExileCreatureBeforeResolution(){ + addCard(Zone.BATTLEFIELD, playerA, "Plains", 5); + addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); + addCard(Zone.HAND,playerA,UNFINISHEDBUSINESS); + addCard(Zone.GRAVEYARD, playerA, EQUIPMENT); + addCard(Zone.GRAVEYARD, playerA, AURA); + addCard(Zone.GRAVEYARD, playerA, BEAR); + // Apostle of Purifying Light has an activated ability to exile card from graveyard + addCard(Zone.BATTLEFIELD, playerB, APOSTLE); + // Nexus wardens gain life if an enchantment entered the battlefield + // This is to test if the aura enters the battlefield + addCard(Zone.BATTLEFIELD, playerA, "Nexus Wardens"); + + setStrictChooseMode(true); + + // Cast Unfinished Business targeting GrizzlyBears, and aura and an equipment + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, UNFINISHEDBUSINESS); + addTarget(playerA, BEAR); + addTarget(playerA, EQUIPMENT+"^"+AURA); + // Exile Grizzly Bears from graveyard before spell resolution + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerB,"{2}: ",BEAR,UNFINISHEDBUSINESS); + waitStackResolved(1, PhaseStep.END_TURN); + + execute(); + + // Grizzly Bears should be exiled + assertExileCount(playerA, BEAR, 1); + // The aura should still be in the graveyard and should never have entered + assertGraveyardCount(playerA, AURA, 1); + assertLife(playerA, 20); + // The equipment should have returned to the battlefield + assertPermanentCount(playerA,EQUIPMENT, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 0e6a10c74f4..a24ee16c362 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -1556,24 +1556,27 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public void assertTopCardRevealed(TestPlayer player, boolean isRevealed) { Assert.assertEquals(isRevealed, player.isTopCardRevealed()); } - - public void assertIsAttachedTo(TestPlayer thePlayer, String theAttachment, String thePermanent) { - + /** + * Asserts if, or if not, theAttachment is attached to thePermanent. + * + * @param isAttached true => assertIsAttachedTo, false => assertIsNotAttachedTo + */ + public void assertAttachedTo(TestPlayer thePlayer, String theAttachment, String thePermanent, boolean isAttached) { List permanents = currentGame.getBattlefield().getAllActivePermanents().stream() .filter(permanent -> permanent.isControlledBy(thePlayer.getId())) .filter(permanent -> permanent.getName().equals(thePermanent)) .collect(Collectors.toList()); - assertTrue(theAttachment + " was not attached to " + thePermanent, + assertTrue(theAttachment + " was "+ (!isAttached ? "":"not") +" attached to " + thePermanent, + !isAttached ^ permanents.stream() .anyMatch(permanent -> permanent.getAttachments() .stream() .map(id -> currentGame.getCard(id)) .map(MageObject::getName) .collect(Collectors.toList()).contains(theAttachment))); - - } + public Permanent getPermanent(String cardName, UUID controller) { assertAliaseSupportInActivateCommand(cardName, false); Permanent found = null;