From 11ddfa0087519b434eb594e20285c8fdba0744d9 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 2 Mar 2024 14:49:17 +0400 Subject: [PATCH] Manifest abilities - improved combo support for MDF, split and other cards (related to #10803, part of #11873) --- .../abilities/keywords/ManifestTest.java | 97 +++++++++++++++++++ .../java/org/mage/test/player/TestPlayer.java | 1 + .../BecomesFaceDownCreatureEffect.java | 21 ++++ .../effects/keyword/ManifestEffect.java | 20 ++-- .../mage/game/permanent/PermanentCard.java | 6 ++ 5 files changed, 136 insertions(+), 9 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java index bf9c21d1ac95..363e4f5b3273 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/ManifestTest.java @@ -1,5 +1,6 @@ package org.mage.test.cards.abilities.keywords; +import mage.MageObject; import mage.cards.Card; import mage.cards.repository.TokenRepository; import mage.constants.EmptyNames; @@ -118,6 +119,102 @@ public void test_Simple_ManifestFromOpponentLibrary() { Assert.assertEquals("manifested cards must be taken from opponent's library", 2, playerA.getLibrary().size() - playerB.getLibrary().size()); } + private void runManifestThenBlink(String cardToManifest, String cardAfterBlink) { + // split, mdfc and other cards must be able to manifested + // bug: https://github.com/magefree/mage/issues/10608 + skipInitShuffling(); + + // Manifest the top card of your library. + addCard(Zone.HAND, playerA, "Soul Summons", 1); // {1}{W}, sorcery + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // + // Exile target creature you control, then return that card to the battlefield under your control. + addCard(Zone.HAND, playerA, "Cloudshift", 1); // {W}, instant + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + // + addCard(Zone.LIBRARY, playerA, cardToManifest, 1); + + // manifest + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("need face down", 1, PhaseStep.PRECOMBAT_MAIN, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + + // blink + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cloudshift", EmptyNames.FACE_DOWN_CREATURE.toString()); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + checkPermanentCount("need no face down", 1, PhaseStep.PRECOMBAT_MAIN, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + + runCode("after blink", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + if (cardAfterBlink == null) { + Assert.assertEquals("after blink card must keep in exile", + 1, currentGame.getExile().getAllCardsByRange(currentGame, playerA.getId()).size()); + } else { + String realPermanentName = currentGame.getBattlefield().getAllPermanents() + .stream() + .filter(p -> p.getName().equals(cardAfterBlink)) + .map(MageObject::getName) + .findFirst() + .orElse(null); + Assert.assertEquals("after blink card must go to battlefield", + cardAfterBlink, realPermanentName); + } + }); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_ManifestThenBlink_Creature() { + runManifestThenBlink("Grizzly Bears", "Grizzly Bears"); + } + + @Test + public void test_ManifestThenBlink_Instant() { + runManifestThenBlink("Lightning Bolt", null); + } + + @Test + public void test_ManifestThenBlink_MDFC_Creature() { + runManifestThenBlink("Akoum Warrior // Akoum Teeth", "Akoum Warrior"); + } + + @Test + public void test_ManifestThenBlink_MDFC_LandOnMainSide() { + runManifestThenBlink("Barkchannel Pathway // Tidechannel Pathway", "Barkchannel Pathway"); + } + + @Test + public void test_ManifestThenBlink_MDFC_LandOnSecondSide() { + runManifestThenBlink("Bala Ged Recovery // Bala Ged Sanctuary", null); + } + + @Test + public void test_ManifestThenBlink_Split_Normal() { + runManifestThenBlink("Assault // Battery", null); + } + + @Test + public void test_ManifestThenBlink_Split_Fused() { + runManifestThenBlink("Alive // Well", null); + } + + @Test + public void test_ManifestThenBlink_Split_Aftermath() { + runManifestThenBlink("Dusk // Dawn", null); + } + + @Test + public void test_ManifestThenBlink_Meld() { + runManifestThenBlink("Graf Rats", "Graf Rats"); + } + + @Test + public void test_ManifestThenBlink_Adventure() { + runManifestThenBlink("Ardenvale Tactician // Dizzying Swoop", "Ardenvale Tactician"); + } + /** * Tests that ETB triggered abilities did not trigger for manifested cards */ diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index cbedb965b368..f4886175e373 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -1219,6 +1219,7 @@ private void printPermanents(Game game, List cards, Player controller .map(c -> (((c instanceof PermanentToken) ? "[T] " : "[C] ") + c.getIdName() + (c.isCopy() ? " [copy of " + c.getCopyFrom().getId().toString().substring(0, 3) + "]" : "") + + " class " + c.getMainCard().getClass().getSimpleName() + "" + " - " + c.getPower().getValue() + "/" + c.getToughness().getValue() + (c.isPlaneswalker(game) ? " - L" + c.getCounters(game).getCount(CounterType.LOYALTY) : "") + ", " + (c.isTapped() ? "Tapped" : "Untapped") diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureEffect.java index fae5ada5d09b..2fffbd3c9537 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureEffect.java @@ -15,6 +15,7 @@ import mage.abilities.keyword.WardAbility; import mage.cards.Card; import mage.cards.CardImpl; +import mage.cards.ModalDoubleFacedCard; import mage.cards.repository.TokenInfo; import mage.cards.repository.TokenRepository; import mage.constants.*; @@ -31,6 +32,9 @@ /** * Support different face down types: morph/manifest and disguise/cloak + *

+ * WARNING, if you use it custom effect then must create it for left side card, + * not main (see findDefaultCardSideForFaceDown) * *

* This effect lets the card be a 2/2 face-down creature, with no text, no name, @@ -334,4 +338,21 @@ public static void makeFaceDownObject(Game game, UUID sourceId, MageObject objec } } + /** + * There are cards with multiple sides like MDFC, but face down must use only main/left side all the time. + * So try to find that side. + */ + public static Card findDefaultCardSideForFaceDown(Game game, Card card) { + // rules example: + // If a double-faced card is manifested, it will be put onto the battlefield face down. While face down, + // it can't transform. If the front face of the card is a creature card, you can turn it face up by paying + // its mana cost. If you do, its front face will be up. + + if (card instanceof ModalDoubleFacedCard) { + // only MDFC uses independent card sides on 2024 + return ((ModalDoubleFacedCard) card).getLeftHalfCard(); + } else { + return card; + } + } } diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java index f65d157d0145..6eb8a15eb360 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java @@ -58,10 +58,6 @@ * 701.34g TODO: need support it * If a manifested permanent that’s represented by an instant or sorcery card would turn face up, its controller * reveals it and leaves it face down. Abilities that trigger whenever a permanent is turned face up won’t trigger. - *

- * 701.34g - * If a manifested permanent that’s represented by an instant or sorcery card would turn face up, its controller - * reveals it and leaves it face down. Abilities that trigger whenever a permanent is turned face up won’t trigger. * * @author LevelX2, JayDi85 */ @@ -123,16 +119,19 @@ public static boolean doManifestCards(Game game, Ability source, Player manifest // prepare face down effect for battlefield permanents // TODO: need research - why it add effect before move?! for (Card card : cardsToManifest) { + Card battlefieldCard = BecomesFaceDownCreatureEffect.findDefaultCardSideForFaceDown(game, card); + // search mana cost for a face up ability (look at face side of the double side card) ManaCosts manaCosts = null; - if (card.isCreature(game)) { - manaCosts = card.getSpellAbility() != null ? card.getSpellAbility().getManaCosts() : null; + if (battlefieldCard.isCreature(game)) { + manaCosts = battlefieldCard.getSpellAbility() != null ? battlefieldCard.getSpellAbility().getManaCosts() : null; if (manaCosts == null) { manaCosts = new ManaCostsImpl<>("{0}"); } } + // zcc + 1 for use case with Rally the Ancestors (see related test) - MageObjectReference objectReference = new MageObjectReference(card.getId(), card.getZoneChangeCounter(game) + 1, game); + MageObjectReference objectReference = new MageObjectReference(battlefieldCard.getId(), battlefieldCard.getZoneChangeCounter(game) + 1, game); game.addEffect(new BecomesFaceDownCreatureEffect(manaCosts, objectReference, Duration.Custom, FaceDownType.MANIFESTED), newSource); } @@ -140,13 +139,16 @@ public static boolean doManifestCards(Game game, Ability source, Player manifest // TODO: possible buggy for multiple cards, see rule 701.34e - it require manifest one by one (card to check: Omarthis, Ghostfire Initiate) manifestPlayer.moveCards(cardsToManifest, Zone.BATTLEFIELD, source, game, false, true, false, null); for (Card card : cardsToManifest) { - Permanent permanent = game.getPermanent(card.getId()); + Card battlefieldCard = BecomesFaceDownCreatureEffect.findDefaultCardSideForFaceDown(game, card); + + Permanent permanent = game.getPermanent(battlefieldCard.getId()); if (permanent != null) { - // TODO: why it set manifested here (face down effect doesn't work?!) + // TODO: permanent already has manifested status, so code can be deleted later // TODO: add test with battlefield trigger/watcher (must not see normal card, must not see face down status without manifest) permanent.setManifested(true); } else { // TODO: looks buggy, card can't be moved to battlefield, but face down effect already active + // or it can be face down on another move to battalefield } } diff --git a/Mage/src/main/java/mage/game/permanent/PermanentCard.java b/Mage/src/main/java/mage/game/permanent/PermanentCard.java index ab3ac2580b7e..1d368d1ecb59 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentCard.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentCard.java @@ -56,6 +56,12 @@ public PermanentCard(Card card, UUID controllerId, Game game) { goodForBattlefield = false; } } + + // face down cards allows in any forms (only face up restricted for non-permanents) + if (card.isFaceDown(game)) { + goodForBattlefield = true; + } + if (!goodForBattlefield) { throw new IllegalArgumentException("Wrong code usage: can't create permanent card from split or mdf: " + card.getName()); }