From b1b83dc5b81ed9587d9d63f0ea40a79bab2a783a Mon Sep 17 00:00:00 2001 From: Cameron Merkel <44722506+Cguy7777@users.noreply.github.com> Date: Fri, 3 May 2024 23:46:58 -0500 Subject: [PATCH] [MKM] Implement Coveted Falcon (#12057) * [MKM] Implement Coveted Falcon * Rework to use OneShotEffect with new approach * Use static ZCC to be safe * Add tests * Remove check in GainControlTargetEffect --- Mage.Sets/src/mage/cards/c/CovetedFalcon.java | 119 +++++++++++ .../src/mage/sets/MurdersAtKarlovManor.java | 2 + .../cards/single/mkm/CovetedFalconTest.java | 201 ++++++++++++++++++ .../continuous/GainControlTargetEffect.java | 3 +- 4 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/CovetedFalcon.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/CovetedFalconTest.java diff --git a/Mage.Sets/src/mage/cards/c/CovetedFalcon.java b/Mage.Sets/src/mage/cards/c/CovetedFalcon.java new file mode 100644 index 00000000000..de6ba2f7608 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CovetedFalcon.java @@ -0,0 +1,119 @@ +package mage.cards.c; + +import java.util.List; +import java.util.UUID; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.AttacksTriggeredAbility; +import mage.abilities.common.TurnedFaceUpSourceTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainControlTargetEffect; +import mage.constants.*; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.DisguiseAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.filter.FilterPermanent; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledPermanent; +import mage.target.common.TargetOpponent; +import mage.target.targetpointer.FixedTarget; + +/** + * @author Cguy7777 + */ +public final class CovetedFalcon extends CardImpl { + + private static final FilterPermanent filter + = new FilterPermanent("permanent you own but don't control"); + + static { + filter.add(TargetController.YOU.getOwnerPredicate()); + filter.add(TargetController.NOT_YOU.getControllerPredicate()); + } + + public CovetedFalcon(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{1}{U}{U}"); + + this.subtype.add(SubType.BIRD); + this.power = new MageInt(1); + this.toughness = new MageInt(4); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Whenever Coveted Falcon attacks, gain control of target permanent you own but don't control. + Ability attacksTriggeredAbility = new AttacksTriggeredAbility(new GainControlTargetEffect(Duration.Custom)); + attacksTriggeredAbility.addTarget(new TargetPermanent(filter)); + this.addAbility(attacksTriggeredAbility); + + // Disguise {1}{U} + this.addAbility(new DisguiseAbility(this, new ManaCostsImpl<>("{1}{U}"))); + + // When Coveted Falcon is turned face up, target opponent gains control of any number of target permanents you control. + // Draw a card for each one they gained control of this way. + Ability turnedFaceUpTriggeredAbility = new TurnedFaceUpSourceTriggeredAbility(new CovetedFalconEffect()); + turnedFaceUpTriggeredAbility.addTarget(new TargetOpponent()); + turnedFaceUpTriggeredAbility.addTarget( + new TargetControlledPermanent(0, Integer.MAX_VALUE, StaticFilters.FILTER_CONTROLLED_PERMANENT, false)); + this.addAbility(turnedFaceUpTriggeredAbility); + } + + private CovetedFalcon(final CovetedFalcon card) { + super(card); + } + + @Override + public CovetedFalcon copy() { + return new CovetedFalcon(this); + } +} + +class CovetedFalconEffect extends OneShotEffect { + + CovetedFalconEffect() { + super(Outcome.Benefit); + staticText = "target opponent gains control of any number of target permanents you control. " + + "Draw a card for each one they gained control of this way"; + } + + private CovetedFalconEffect(final CovetedFalconEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + UUID targetOpponentId = source.getFirstTarget(); + List targetPermanentIds = source.getTargets().get(1).getTargets(); + for (UUID permanentId : targetPermanentIds) { + game.addEffect( + new GainControlTargetEffect(Duration.Custom, true, targetOpponentId) + .setTargetPointer(new FixedTarget(permanentId, game)), + source); + } + + game.getState().processAction(game); + + int cardsToDraw = 0; + for (UUID permanentId : targetPermanentIds) { + Permanent permanent = game.getPermanent(permanentId); + if (permanent != null && permanent.isControlledBy(targetOpponentId)) { + cardsToDraw++; + } + } + + new DrawCardSourceControllerEffect(cardsToDraw).apply(game, source); + return true; + } + + @Override + public CovetedFalconEffect copy() { + return new CovetedFalconEffect(this); + } +} diff --git a/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java b/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java index d687d92e176..b7f173949ae 100644 --- a/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java +++ b/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java @@ -86,6 +86,8 @@ public final class MurdersAtKarlovManor extends ExpansionSet { cards.add(new SetCardInfo("Conspiracy Unraveler", 47, Rarity.MYTHIC, mage.cards.c.ConspiracyUnraveler.class)); cards.add(new SetCardInfo("Convenient Target", 119, Rarity.UNCOMMON, mage.cards.c.ConvenientTarget.class)); cards.add(new SetCardInfo("Cornered Crook", 120, Rarity.UNCOMMON, mage.cards.c.CorneredCrook.class)); + cards.add(new SetCardInfo("Coveted Falcon", 48, Rarity.RARE, mage.cards.c.CovetedFalcon.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Coveted Falcon", 393, Rarity.RARE, mage.cards.c.CovetedFalcon.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Crime Novelist", 121, Rarity.UNCOMMON, mage.cards.c.CrimeNovelist.class)); cards.add(new SetCardInfo("Crimestopper Sprite", 49, Rarity.COMMON, mage.cards.c.CrimestopperSprite.class)); cards.add(new SetCardInfo("Crowd-Control Warden", 193, Rarity.COMMON, mage.cards.c.CrowdControlWarden.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/CovetedFalconTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/CovetedFalconTest.java new file mode 100644 index 00000000000..fd367a14e24 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mkm/CovetedFalconTest.java @@ -0,0 +1,201 @@ +package org.mage.test.cards.single.mkm; + +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 CovetedFalconTest extends CardTestPlayerBase { + + /* + * Coveted Falcon {1}{U}{U} + * Artifact Creature - Bird (1/4) + * Flying + * Whenever Coveted Falcon attacks, gain control of target permanent you own but don't control. + * Disguise {1}{U} + * When Coveted Falcon is turned face up, target opponent gains control of any number of target permanents you control. + * Draw a card for each one they gained control of this way. + */ + + @Test + public void test_GiveControl() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3 + 2); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + addCard(Zone.HAND, playerA, "Coveted Falcon", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Coveted Falcon using Disguise", true); + + // When Coveted Falcon is turned face up, target opponent gains control of any number of target permanents you control. + // Draw a card for each one they gained control of this way. + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); + addTarget(playerA, playerB); + addTarget(playerA, "Grizzly Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, "Grizzly Bears", 1); + assertHandCount(playerA, 1); + } + + @Test + public void test_TargetChangesControllerInResponse() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3 + 2); + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); + addCard(Zone.HAND, playerA, "Coveted Falcon", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Mountain", 5); + // Turn Against {4}{R} + // Instant + // Devoid + // Gain control of target creature until end of turn. Untap that creature. It gains haste until end of turn. + addCard(Zone.HAND, playerB, "Turn Against", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Coveted Falcon using Disguise", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); + addTarget(playerA, playerB); + addTarget(playerA, "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Turn Against", "Grizzly Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, "Grizzly Bears", 1); + assertHandCount(playerA, 0); + } + + @Test + public void test_TargetLeavesAndReturnsUnderYourControlInResponse() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3 + 2); + addCard(Zone.BATTLEFIELD, playerA, "Putrid Goblin", 1); // 2/2 Zombie Goblin w/ Persist + addCard(Zone.HAND, playerA, "Coveted Falcon", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); + addCard(Zone.HAND, playerB, "Murder", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Coveted Falcon using Disguise", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); + addTarget(playerA, playerB); + addTarget(playerA, "Putrid Goblin"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Murder", "Putrid Goblin"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, "Putrid Goblin", 0); + assertHandCount(playerA, 0); + } + + @Test + public void test_TargetLeavesAndReturnsUnderOpponentsControlInResponse() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3 + 2); + // Treacherous Pit-Dweller {B}{B} + // Creature - Demon (4/3) + // When Treacherous Pit-Dweller enters the battlefield from a graveyard, target opponent gains control of it. + // Undying + addCard(Zone.BATTLEFIELD, playerA, "Treacherous Pit-Dweller", 1); + addCard(Zone.HAND, playerA, "Coveted Falcon", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); + addCard(Zone.HAND, playerB, "Murder", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Coveted Falcon using Disguise", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); + addTarget(playerA, playerB); + addTarget(playerA, "Treacherous Pit-Dweller"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Murder", "Treacherous Pit-Dweller"); + // When Treacherous Pit-Dweller enters the battlefield from a graveyard, target opponent gains control of it. + addTarget(playerA, playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, "Treacherous Pit-Dweller", 1); + assertHandCount(playerA, 0); + } + + /* + * Guardian Beast {3}{B} + * Creature - Beast (2/4) + * As long as Guardian Beast is untapped, noncreature artifacts you control can't be enchanted, + * they have indestructible, and other players can't gain control of them. + * This effect doesn't remove Auras already attached to those artifacts. + */ + + // When you turn Coveted Falcon face up while controlling Guardian Beast, you can target noncreature artifacts, + // but your opponent shouldn't gain control of them, and you shouldn't draw cards for them. + // If Guardian Beast dies after Falcon's turn-up trigger resolves, you should still keep those artifacts. + @Test + public void test_GiveArtifactAndNonartifactWhileControllingGuardianBeast() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3 + 2); + addCard(Zone.BATTLEFIELD, playerA, "Guardian Beast", 1); + addCard(Zone.BATTLEFIELD, playerA, "Darksteel Relic", 1); // Artifact w/ Indestructible + addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1); // Should be given away normally + addCard(Zone.HAND, playerA, "Coveted Falcon", 1); + + addCard(Zone.BATTLEFIELD, playerB, "Swamp", 3); + addCard(Zone.HAND, playerB, "Murder", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Coveted Falcon using Disguise", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); + addTarget(playerA, playerB); + addTarget(playerA, "Darksteel Relic^Grizzly Bears"); + + castSpell(1, PhaseStep.BEGIN_COMBAT, playerB, "Murder", "Guardian Beast"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, "Darksteel Relic", 0); + assertPermanentCount(playerB, "Grizzly Bears", 1); + assertHandCount(playerA, 1); + } + + + // If you target Guardian Beast AND one or more noncreature artifacts to give away, you should only give away the Beast. + // Test is duplicated to catch glitches that only occur when the targets are ordered a certain way. + // TODO Doesn't work properly + @Test + @Ignore + public void test_GiveGuardianBeastAndArtifactsA() { + setupGiveGuardianBeastAndArtifactsTest(true); + } + + @Test + @Ignore + public void test_GiveGuardianBeastAndArtifactsB() { + setupGiveGuardianBeastAndArtifactsTest(false); + } + + private void setupGiveGuardianBeastAndArtifactsTest(final boolean guardianBeastFirst) { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3 + 2); + addCard(Zone.BATTLEFIELD, playerA, "Guardian Beast", 1); + addCard(Zone.BATTLEFIELD, playerA, "Darksteel Relic", 1); + addCard(Zone.HAND, playerA, "Coveted Falcon", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Coveted Falcon using Disguise", true); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}{U}: Turn this face-down permanent face up."); + addTarget(playerA, playerB); + addTarget(playerA, guardianBeastFirst ? "Guardian Beast^Darksteel Relic" : "Darksteel Relic^Guardian Beast"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerB, "Guardian Beast", 1); + assertPermanentCount(playerB, "Darksteel Relic", 0); + assertHandCount(playerA, 1); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainControlTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainControlTargetEffect.java index 70045caf8c2..6a8c2cf00d9 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainControlTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainControlTargetEffect.java @@ -105,8 +105,7 @@ public class GainControlTargetEffect extends ContinuousEffectImpl { controlChanged = true; } } - if (source.isActivatedAbility() - && firstControlChange && !controlChanged) { + if (firstControlChange && !controlChanged) { // If it was not possible to get control of target permanent by the activated ability the first time it took place // the effect failed (e.g. because of Guardian Beast) and must be discarded // This does not handle correctly multiple targets at once