From faa868aa166fbdbbe66324ab81a95ec29393e061 Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Sun, 26 May 2024 16:17:14 +0200 Subject: [PATCH] implement [MH3] Detective's Phoenix --- .../src/mage/cards/d/DetectivesPhoenix.java | 104 +++++++++++++++ Mage.Sets/src/mage/sets/ModernHorizons3.java | 1 + .../single/mh3/DetectivesPhoenixTest.java | 126 ++++++++++++++++++ .../mage/abilities/keyword/BestowAbility.java | 13 +- 4 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 Mage.Sets/src/mage/cards/d/DetectivesPhoenix.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/DetectivesPhoenixTest.java diff --git a/Mage.Sets/src/mage/cards/d/DetectivesPhoenix.java b/Mage.Sets/src/mage/cards/d/DetectivesPhoenix.java new file mode 100644 index 00000000000..a4ca1397611 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DetectivesPhoenix.java @@ -0,0 +1,104 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.common.CollectEvidenceCost; +import mage.abilities.effects.AsThoughEffectImpl; +import mage.abilities.effects.common.continuous.BoostEnchantedEffect; +import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect; +import mage.abilities.keyword.BestowAbility; +import mage.abilities.keyword.FlyingAbility; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.game.Game; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class DetectivesPhoenix extends CardImpl { + + public DetectivesPhoenix(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT, CardType.CREATURE}, "{2}{R}"); + + this.subtype.add(SubType.PHOENIX); + this.power = new MageInt(2); + this.toughness = new MageInt(2); + + // Bestow--{R}, Collect evidence 6 + Ability ability = new BestowAbility(this, "{R}"); + ability.addCost(new CollectEvidenceCost(6)); + this.addAbility(ability); + + // Flying + this.addAbility(FlyingAbility.getInstance()); + + // Haste + this.addAbility(HasteAbility.getInstance()); + + // Enchanted creature gets +2/+2 and has flying and haste. + ability = new SimpleStaticAbility(new BoostEnchantedEffect(2, 2)); + ability.addEffect( + new GainAbilityAttachedEffect(FlyingAbility.getInstance(), AttachmentType.AURA) + .setText("and has flying") + ); + ability.addEffect( + new GainAbilityAttachedEffect(HasteAbility.getInstance(), AttachmentType.AURA) + .setText("and haste") + ); + this.addAbility(ability); + + // You may cast Detective's Phoenix from your graveyard using its bestow ability. + this.addAbility(new SimpleStaticAbility(Zone.ALL, new DetectivesPhoenixEffect())); + } + + private DetectivesPhoenix(final DetectivesPhoenix card) { + super(card); + } + + @Override + public DetectivesPhoenix copy() { + return new DetectivesPhoenix(this); + } +} + +// Similar to Tenacious Underdog +class DetectivesPhoenixEffect extends AsThoughEffectImpl { + + DetectivesPhoenixEffect() { + super(AsThoughEffectType.CAST_FROM_NOT_OWN_HAND_ZONE, Duration.EndOfGame, Outcome.PutCreatureInPlay); + staticText = "You may cast {this} from your graveyard using its bestow ability"; + } + + private DetectivesPhoenixEffect(final DetectivesPhoenixEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public DetectivesPhoenixEffect copy() { + return new DetectivesPhoenixEffect(this); + } + + @Override + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { + return objectId.equals(source.getSourceId()) + && source.isControlledBy(playerId) + && affectedAbility instanceof BestowAbility + && game.getState().getZone(source.getSourceId()) == Zone.GRAVEYARD + && game.getCard(source.getSourceId()) != null; + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + return false; + } +} diff --git a/Mage.Sets/src/mage/sets/ModernHorizons3.java b/Mage.Sets/src/mage/sets/ModernHorizons3.java index a0bc546208a..6ca5e6ebf2b 100644 --- a/Mage.Sets/src/mage/sets/ModernHorizons3.java +++ b/Mage.Sets/src/mage/sets/ModernHorizons3.java @@ -41,6 +41,7 @@ public final class ModernHorizons3 extends ExpansionSet { cards.add(new SetCardInfo("Cursed Mirror", 279, Rarity.RARE, mage.cards.c.CursedMirror.class)); cards.add(new SetCardInfo("Deep Analysis", 268, Rarity.UNCOMMON, mage.cards.d.DeepAnalysis.class)); cards.add(new SetCardInfo("Deserted Temple", 301, Rarity.RARE, mage.cards.d.DesertedTemple.class)); + cards.add(new SetCardInfo("Detective's Phoenix", 116, Rarity.RARE, mage.cards.d.DetectivesPhoenix.class)); cards.add(new SetCardInfo("Devourer of Destiny", 2, Rarity.RARE, mage.cards.d.DevourerOfDestiny.class)); cards.add(new SetCardInfo("Disciple of Freyalise", 250, Rarity.UNCOMMON, mage.cards.d.DiscipleOfFreyalise.class)); cards.add(new SetCardInfo("Dreadmobile", 87, Rarity.UNCOMMON, mage.cards.d.Dreadmobile.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/DetectivesPhoenixTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/DetectivesPhoenixTest.java new file mode 100644 index 00000000000..62f5efeeeeb --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/mh3/DetectivesPhoenixTest.java @@ -0,0 +1,126 @@ +package org.mage.test.cards.single.mh3; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class DetectivesPhoenixTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.d.DetectivesPhoenix Detective's Phoenix} {2}{R} + * Enchantment Creature — Phoenix + * Bestow—{R}, Collect evidence 6. (To pay this bestow cost, pay {R} and exile cards with total mana value 6 or greater from your graveyard.) + * Flying, haste + * Enchanted creature gets +2/+2 and has flying and haste. + * You may cast Detective's Phoenix from your graveyard using its bestow ability. + * 2/2 + */ + private static final String phoenix = "Detective's Phoenix"; + + @Test + public void test_Cast_FromHand_Normal() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + addCard(Zone.HAND, playerA, phoenix); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix, true); + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix + " using bestow", false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, phoenix); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertTappedCount("Mountain", true, 3); + assertPowerToughness(playerA, phoenix, 2, 2); + } + + @Test + public void test_Cast_FromHand_Bestow() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + addCard(Zone.HAND, playerA, phoenix); + addCard(Zone.GRAVEYARD, playerA, "Grave Titan"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix, true); + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix + " using bestow", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, phoenix + " using bestow", "Memnite"); + setChoice(playerA, "Grave Titan"); // pay for Collect Evidence. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertTappedCount("Mountain", true, 1); + assertExileCount("Grave Titan", 1); + assertPowerToughness(playerA, "Memnite", 1 + 2, 1 + 2); + } + + @Test + public void test_Cast_FromGraveyard_Normal_NotPossible() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + addCard(Zone.GRAVEYARD, playerA, phoenix); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix, false); + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix + " using bestow", false); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + } + + @Test + public void test_Cast_FromGraveyard_Bestow() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + addCard(Zone.GRAVEYARD, playerA, phoenix); + addCard(Zone.GRAVEYARD, playerA, "Grave Titan"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix, true); // It is a prefix check, so sadly can not check that can't cast using non-bestow + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix + " using bestow", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, phoenix + " using bestow", "Memnite"); + setChoice(playerA, "Grave Titan"); // pay for Collect Evidence. + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertTappedCount("Mountain", true, 1); + assertExileCount("Grave Titan", 1); + assertPowerToughness(playerA, "Memnite", 1 + 2, 1 + 2); + } + + @Test + public void test_Cast_FromGraveyard_BestowPossible_NotRegular() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Memnite"); + addCard(Zone.GRAVEYARD, playerA, phoenix); + addCard(Zone.GRAVEYARD, playerA, "Grave Titan"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix, true); // It is a prefix check, so sadly can not check that can't cast using non-bestow + checkPlayableAbility("Can cast normal way", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast " + phoenix + " using bestow", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, phoenix, "Memnite"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + try { + execute(); + Assert.fail("should have failed"); + } catch (Throwable e) { + if (!e.getMessage().contains("Cast " + phoenix)) { + Assert.fail("Should have thrown error about not being able to cast the Phoenix, but got:\n" + e.getMessage()); + } + } + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/BestowAbility.java b/Mage/src/main/java/mage/abilities/keyword/BestowAbility.java index 13bc3982967..d4e25afec89 100644 --- a/Mage/src/main/java/mage/abilities/keyword/BestowAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/BestowAbility.java @@ -4,6 +4,7 @@ import mage.MageObject; import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.costs.Costs; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.ReplacementEffectImpl; import mage.abilities.effects.common.AttachEffect; @@ -15,6 +16,7 @@ import mage.game.events.GameEvent.EventType; import mage.game.permanent.Permanent; import mage.target.TargetPermanent; import mage.target.common.TargetCreaturePermanent; +import mage.util.CardUtil; /** * 702.102. Bestow @@ -109,7 +111,16 @@ public class BestowAbility extends SpellAbility { @Override public String getRule() { - return "Bestow " + getManaCostsToPay().getText() + " (If you cast this card for its bestow cost, it's an Aura spell with enchant creature. It becomes a creature again if it's not attached to a creature.)"; + StringBuilder sb = new StringBuilder("Bestow"); + Costs costs = getCosts(); + if (costs.size() > 0) { + sb.append("—").append(getManaCostsToPay().getText()).append(", "); + sb.append(CardUtil.getTextWithFirstCharUpperCase(costs.getText())).append('.'); + } else { + sb.append(" ").append(getManaCostsToPay().getText()); + } + sb.append(" (If you cast this card for its bestow cost, it's an Aura spell with enchant creature. It becomes a creature again if it's not attached to a creature.)"); + return sb.toString(); } public static void becomeCreature(Permanent permanent, Game game) {