From f5168e7c98907cade907389deee820269b030b83 Mon Sep 17 00:00:00 2001 From: ssk97 Date: Thu, 4 Apr 2024 13:21:25 -0700 Subject: [PATCH] [MKM] Implement 3x cards (#12020) * Agency Outfitter * Bite Down on Crime * Bubble Smuggler (and test for Hooded Hydra's similar ability) * Frantic Scapegoat * Immediate review comments, will investigate further Agency Outfitter improvements * Remove Agency Outfitter, will be a separate PR * Fix Scapegoat not losing suspected * Fix import * use ZoneChangeBatchEvent instead of ZoneChangeGroupEvent * Move suspected check to checkInterveningIfClause --- .../src/mage/cards/b/BiteDownOnCrime.java | 72 ++++++++ .../src/mage/cards/b/BubbleSmuggler.java | 52 ++++++ .../src/mage/cards/f/FranticScapegoat.java | 162 ++++++++++++++++++ Mage.Sets/src/mage/cards/h/HoodedHydra.java | 1 - .../src/mage/sets/MurdersAtKarlovManor.java | 3 + .../cards/abilities/keywords/MorphTest.java | 19 ++ 6 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java create mode 100644 Mage.Sets/src/mage/cards/b/BubbleSmuggler.java create mode 100644 Mage.Sets/src/mage/cards/f/FranticScapegoat.java diff --git a/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java new file mode 100644 index 00000000000..17408d3d23b --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BiteDownOnCrime.java @@ -0,0 +1,72 @@ +package mage.cards.b; + +import mage.abilities.Ability; +import mage.abilities.condition.common.CollectedEvidenceCondition; +import mage.abilities.costs.CostAdjuster; +import mage.abilities.costs.OptionalAdditionalCost; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.DamageWithPowerFromOneToAnotherTargetEffect; +import mage.abilities.effects.common.InfoEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.keyword.CollectEvidenceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.target.common.TargetControlledCreaturePermanent; +import mage.target.common.TargetCreaturePermanent; +import mage.util.CardUtil; + +import java.util.UUID; + +/** + * + * @author notgreat + */ +public final class BiteDownOnCrime extends CardImpl { + + public BiteDownOnCrime(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{G}"); + + // As an additional cost to cast this spell, you may collect evidence 6. This spell costs {2} less to cast if evidence was collected. + this.addAbility(new CollectEvidenceAbility(6)); + + this.getSpellAbility().addEffect(new InfoEffect("this spell costs {2} less to cast if evidence was collected")); + this.getSpellAbility().setCostAdjuster(BiteDownOnCrimeAdjuster.instance); + + // Target creature you control gets +2/+0 until end of turn. + Effect effect = new BoostTargetEffect(2, 0, Duration.EndOfTurn); + this.getSpellAbility().addEffect(effect); + this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent()); + + // It deals damage equal to its power to target creature you don't control. + this.getSpellAbility().addEffect(new DamageWithPowerFromOneToAnotherTargetEffect("It")); + this.getSpellAbility().addTarget(new TargetCreaturePermanent(StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL)); + } + + private BiteDownOnCrime(final BiteDownOnCrime card) { + super(card); + } + + @Override + public BiteDownOnCrime copy() { + return new BiteDownOnCrime(this); + } +} + + +enum BiteDownOnCrimeAdjuster implements CostAdjuster { + instance; + + private static final OptionalAdditionalCost collectEvidenceCost = CollectEvidenceAbility.makeCost(6); + + @Override + public void adjustCosts(Ability ability, Game game) { + if (CollectedEvidenceCondition.instance.apply(game, ability) + || (game.inCheckPlayableState() && collectEvidenceCost.canPay(ability, null, ability.getControllerId(), game))) { + CardUtil.reduceCost(ability, 2); + } + } +} diff --git a/Mage.Sets/src/mage/cards/b/BubbleSmuggler.java b/Mage.Sets/src/mage/cards/b/BubbleSmuggler.java new file mode 100644 index 00000000000..7e305c24e69 --- /dev/null +++ b/Mage.Sets/src/mage/cards/b/BubbleSmuggler.java @@ -0,0 +1,52 @@ +package mage.cards.b; + +import java.util.UUID; +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.AsTurnedFaceUpEffect; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.constants.SubType; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.keyword.DisguiseAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Zone; +import mage.counters.CounterType; + +/** + * + * @author notgreat + */ +public final class BubbleSmuggler extends CardImpl { + + public BubbleSmuggler(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{1}{U}"); + + this.subtype.add(SubType.OCTOPUS); + this.subtype.add(SubType.FISH); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Disguise {5}{U} + this.addAbility(new DisguiseAbility(this, new ManaCostsImpl<>("{5}{U}"))); + + // As Bubble Smuggler is turned face up, put four +1/+1 counters on it. + Effect effect = new AddCountersSourceEffect(CounterType.P1P1.createInstance(4)); + effect.setText("put four +1/+1 counters on it"); + Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new AsTurnedFaceUpEffect(effect, false)); + ability.setWorksFaceDown(true); + this.addAbility(ability); + } + + private BubbleSmuggler(final BubbleSmuggler card) { + super(card); + } + + @Override + public BubbleSmuggler copy() { + return new BubbleSmuggler(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FranticScapegoat.java b/Mage.Sets/src/mage/cards/f/FranticScapegoat.java new file mode 100644 index 00000000000..feb8fe2daeb --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FranticScapegoat.java @@ -0,0 +1,162 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.SuspectSourceEffect; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.Zone; +import mage.filter.common.FilterCreaturePermanent; +import mage.filter.predicate.permanent.PermanentReferenceInCollectionPredicate; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeBatchEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.Target; +import mage.target.TargetPermanent; + +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author notgreat + */ +public final class FranticScapegoat extends CardImpl { + + public FranticScapegoat(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{R}"); + + this.subtype.add(SubType.GOAT); + this.power = new MageInt(1); + this.toughness = new MageInt(1); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // When Frantic Scapegoat enters the battlefield, suspect it. + this.addAbility(new EntersBattlefieldTriggeredAbility(new SuspectSourceEffect())); + + // Whenever one or more other creatures enter the battlefield under your control, if Frantic Scapegoat is suspected, you may suspect one of the other creatures. If you do, Frantic Scapegoat is no longer suspected. + this.addAbility(new FranticScapegoatTriggeredAbility()); + + } + + private FranticScapegoat(final FranticScapegoat card) { + super(card); + } + + @Override + public FranticScapegoat copy() { + return new FranticScapegoat(this); + } +} + +//Based on Lightmine Field +class FranticScapegoatTriggeredAbility extends TriggeredAbilityImpl { + + FranticScapegoatTriggeredAbility() { + super(Zone.BATTLEFIELD, new FranticScapegoatSuspectEffect(), true); + } + + private FranticScapegoatTriggeredAbility(final FranticScapegoatTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; + } + + @Override + public boolean checkInterveningIfClause(Game game) { + Permanent source = getSourcePermanentIfItStillExists(game); + return (source != null && source.isSuspected()); + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; + Set enteringCreatures = zEvent.getEvents().stream() + .filter(z -> z.getToZone() == Zone.BATTLEFIELD) + .filter(z -> this.controllerId.equals(z.getPlayerId())) + .map(ZoneChangeEvent::getTarget) + .filter(Objects::nonNull) + .filter(permanent -> permanent.isCreature(game)) + .map(p -> new MageObjectReference(p, game)) + .collect(Collectors.toSet()); + if (!enteringCreatures.isEmpty()) { + this.getEffects().setValue("franticScapegoatEnteringCreatures", enteringCreatures); + return true; + } + return false; + } + + @Override + public FranticScapegoatTriggeredAbility copy() { + return new FranticScapegoatTriggeredAbility(this); + } + + @Override + public String getRule() { + return "Whenever one or more other creatures enter the battlefield under your control, if {this} is suspected, you may suspect one of the other creatures. If you do, {this} is no longer suspected."; + } +} + +class FranticScapegoatSuspectEffect extends OneShotEffect { + + FranticScapegoatSuspectEffect() { + super(Outcome.Benefit); + this.staticText = "Suspect one of the other creatures"; + } + + private FranticScapegoatSuspectEffect(final FranticScapegoatSuspectEffect effect) { + super(effect); + } + + @Override + public FranticScapegoatSuspectEffect copy() { + return new FranticScapegoatSuspectEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Set enteringSet = (Set) getValue("franticScapegoatEnteringCreatures"); + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null && enteringSet != null) { + Permanent suspect = null; + if (enteringSet.size() > 1) { + FilterCreaturePermanent filter = new FilterCreaturePermanent("one of those creatures"); + filter.add(new PermanentReferenceInCollectionPredicate(enteringSet)); + Target target = new TargetPermanent(filter); + target.withNotTarget(true); + if (controller.choose(outcome, target, source, game)) { + suspect = game.getPermanent(target.getFirstTarget()); + } + } else { //There is only 1 creature in the set + for (MageObjectReference s : enteringSet) { + suspect = s.getPermanent(game); + } + } + if (suspect != null) { + suspect.setSuspected(true, game, source); + Permanent scapegoat = source.getSourcePermanentIfItStillExists(game); + if (scapegoat != null) { + scapegoat.setSuspected(false, game, source); + } + } + } + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/h/HoodedHydra.java b/Mage.Sets/src/mage/cards/h/HoodedHydra.java index 8483de178c7..c63e5a67216 100644 --- a/Mage.Sets/src/mage/cards/h/HoodedHydra.java +++ b/Mage.Sets/src/mage/cards/h/HoodedHydra.java @@ -49,7 +49,6 @@ public final class HoodedHydra extends CardImpl { // As Hooded Hydra is turned face up, put five +1/+1 counters on it. Effect effect = new AddCountersSourceEffect(CounterType.P1P1.createInstance(5)); effect.setText("put five +1/+1 counters on it"); - // TODO: Does not work because the ability is still removed from permanent while the effect checks if the ability still exists. Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new AsTurnedFaceUpEffect(effect, false)); ability.setWorksFaceDown(true); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java b/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java index 33ad91b1309..f3e28adc36a 100644 --- a/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java +++ b/Mage.Sets/src/mage/sets/MurdersAtKarlovManor.java @@ -45,10 +45,12 @@ public final class MurdersAtKarlovManor extends ExpansionSet { cards.add(new SetCardInfo("Basilica Stalker", 78, Rarity.COMMON, mage.cards.b.BasilicaStalker.class)); cards.add(new SetCardInfo("Behind the Mask", 39, Rarity.COMMON, mage.cards.b.BehindTheMask.class)); cards.add(new SetCardInfo("Benthic Criminologists", 40, Rarity.COMMON, mage.cards.b.BenthicCriminologists.class)); + cards.add(new SetCardInfo("Bite Down on Crime", 154, Rarity.COMMON, mage.cards.b.BiteDownOnCrime.class)); cards.add(new SetCardInfo("Blood Spatter Analysis", 189, Rarity.RARE, mage.cards.b.BloodSpatterAnalysis.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Blood Spatter Analysis", 413, Rarity.RARE, mage.cards.b.BloodSpatterAnalysis.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Bolrac-Clan Basher", 112, Rarity.UNCOMMON, mage.cards.b.BolracClanBasher.class)); cards.add(new SetCardInfo("Branch of Vitu-Ghazi", 258, Rarity.UNCOMMON, mage.cards.b.BranchOfVituGhazi.class)); + cards.add(new SetCardInfo("Bubble Smuggler", 41, Rarity.COMMON, mage.cards.b.BubbleSmuggler.class)); cards.add(new SetCardInfo("Burden of Proof", 42, Rarity.UNCOMMON, mage.cards.b.BurdenOfProof.class)); cards.add(new SetCardInfo("Buried in the Garden", 191, Rarity.UNCOMMON, mage.cards.b.BuriedInTheGarden.class)); cards.add(new SetCardInfo("Call a Surprise Witness", 6, Rarity.UNCOMMON, mage.cards.c.CallASurpriseWitness.class, NON_FULL_USE_VARIOUS)); @@ -120,6 +122,7 @@ public final class MurdersAtKarlovManor extends ExpansionSet { cards.add(new SetCardInfo("Forensic Researcher", 58, Rarity.UNCOMMON, mage.cards.f.ForensicResearcher.class)); cards.add(new SetCardInfo("Forest", 276, Rarity.LAND, mage.cards.basiclands.Forest.class, FULL_ART_BFZ_VARIOUS)); cards.add(new SetCardInfo("Forum Familiar", 16, Rarity.UNCOMMON, mage.cards.f.ForumFamiliar.class)); + cards.add(new SetCardInfo("Frantic Scapegoat", 126, Rarity.UNCOMMON, mage.cards.f.FranticScapegoat.class)); cards.add(new SetCardInfo("Furtive Courier", 59, Rarity.UNCOMMON, mage.cards.f.FurtiveCourier.class)); cards.add(new SetCardInfo("Fuss // Bother", 248, Rarity.UNCOMMON, mage.cards.f.FussBother.class)); cards.add(new SetCardInfo("Gadget Technician", 204, Rarity.COMMON, mage.cards.g.GadgetTechnician.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java index 2c2837c3043..5f8b5fd06a9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/MorphTest.java @@ -1294,4 +1294,23 @@ public class MorphTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); } + + @Test + public void test_Morph_HoodedHydra() { + // Morph {2} + addCard(Zone.HAND, playerA, "Hooded Hydra"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3+5); + + // prepare face down + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Hooded Hydra using Morph"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{3}{G}{G}: Turn this face-down permanent face up."); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertPermanentCount(playerA, "Hooded Hydra", 1); + assertPowerToughness(playerA, "Hooded Hydra", 5, 5); + } }