diff --git a/Mage.Sets/src/mage/cards/s/ScrollOfFate.java b/Mage.Sets/src/mage/cards/s/ScrollOfFate.java index 0d467c260f9..0f42f901154 100644 --- a/Mage.Sets/src/mage/cards/s/ScrollOfFate.java +++ b/Mage.Sets/src/mage/cards/s/ScrollOfFate.java @@ -1,28 +1,20 @@ package mage.cards.s; -import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.SimpleActivatedAbility; import mage.abilities.costs.common.TapSourceCost; -import mage.abilities.costs.mana.ManaCosts; -import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect; -import mage.cards.Card; +import mage.abilities.effects.keyword.ManifestEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; +import mage.cards.CardsImpl; import mage.constants.CardType; -import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.game.Game; import mage.players.Player; import mage.target.common.TargetCardInHand; -import java.util.Objects; -import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; /** * @author TheElk801 @@ -66,42 +58,15 @@ class ScrollOfFateEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller == null - || controller.getHand().isEmpty()) { + if (controller == null) { return false; } + TargetCardInHand targetCard = new TargetCardInHand(); - if (!controller.chooseTarget(Outcome.PutCardInPlay, controller.getHand(), - targetCard, source, game)) { + if (!controller.chooseTarget(Outcome.PutCardInPlay, controller.getHand(), targetCard, source, game)) { return false; } - Ability newSource = source.copy(); - newSource.setWorksFaceDown(true); - Set cards = targetCard - .getTargets() - .stream() - .map(game::getCard) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - cards.stream().forEach(card -> { - ManaCosts manaCosts = null; - if (card.isCreature(game)) { - manaCosts = card.getSpellAbility() != null ? card.getSpellAbility().getManaCosts() : null; - if (manaCosts == null) { - manaCosts = new ManaCostsImpl<>("{0}"); - } - } - MageObjectReference objectReference = new MageObjectReference(card.getId(), - card.getZoneChangeCounter(game) + 1, game); - game.addEffect(new BecomesFaceDownCreatureEffect(manaCosts, objectReference, - Duration.Custom, BecomesFaceDownCreatureEffect.FaceDownType.MANIFESTED), newSource); - }); - controller.moveCards(cards, Zone.BATTLEFIELD, source, game, false, true, false, null); - cards.stream() - .map(Card::getId) - .map(game::getPermanent) - .filter(permanent -> permanent != null) - .forEach(permanent -> permanent.setManifested(true)); - return true; + + return ManifestEffect.doManifestCards(game, source, controller, new CardsImpl(targetCard.getTargets()).getCards(game)); } } diff --git a/Mage.Sets/src/mage/cards/t/ThievingAmalgam.java b/Mage.Sets/src/mage/cards/t/ThievingAmalgam.java index 2e0ef9921b2..cdce1ba8ef1 100644 --- a/Mage.Sets/src/mage/cards/t/ThievingAmalgam.java +++ b/Mage.Sets/src/mage/cards/t/ThievingAmalgam.java @@ -1,26 +1,24 @@ package mage.cards.t; import mage.MageInt; -import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.DiesCreatureTriggeredAbility; -import mage.abilities.costs.mana.ManaCosts; -import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.Effect; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.GainLifeEffect; -import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect; -import mage.cards.Card; +import mage.abilities.effects.keyword.ManifestEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; -import mage.constants.*; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.TargetController; import mage.filter.FilterPermanent; import mage.filter.common.FilterControlledCreaturePermanent; import mage.game.Game; import mage.players.Player; -import java.util.Set; import java.util.UUID; /** @@ -83,31 +81,12 @@ class ThievingAmalgamManifestEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - Player active = game.getPlayer(game.getActivePlayerId()); - if (controller == null || active == null) { + Player targetPlayer = game.getPlayer(game.getActivePlayerId()); + if (controller == null || targetPlayer == null) { return false; } - Ability newSource = source.copy(); - newSource.setWorksFaceDown(true); - Set cards = active.getLibrary().getTopCards(game, 1); - cards.stream().forEach(card -> { - ManaCosts manaCosts = null; - if (card.isCreature(game)) { - manaCosts = card.getSpellAbility() != null ? card.getSpellAbility().getManaCosts() : null; - if (manaCosts == null) { - manaCosts = new ManaCostsImpl<>("{0}"); - } - } - MageObjectReference objectReference = new MageObjectReference(card.getId(), card.getZoneChangeCounter(game) + 1, game); - game.addEffect(new BecomesFaceDownCreatureEffect(manaCosts, objectReference, Duration.Custom, BecomesFaceDownCreatureEffect.FaceDownType.MANIFESTED), newSource); - }); - controller.moveCards(cards, Zone.BATTLEFIELD, source, game, false, true, false, null); - cards.stream() - .map(Card::getId) - .map(game::getPermanent) - .filter(permanent -> permanent != null) - .forEach(permanent -> permanent.setManifested(true)); - return true; + + return ManifestEffect.doManifestCards(game, source, controller, targetPlayer.getLibrary().getTopCards(game, 1)); } } 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 19adde4b20a..bf9c21d1ac9 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 @@ -23,6 +23,101 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class ManifestTest extends CardTestPlayerBase { + @Test + public void test_Simple_ManifestFromOwnLibrary() { + // Manifest the top card of your library. + addCard(Zone.HAND, playerA, "Soul Summons", 1); // {1}{W} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + // manifest + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Soul Summons"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } + + @Test + public void test_Simple_ManifestFromHand() { + // {T}: Manifest a card from your hand. + addCard(Zone.BATTLEFIELD, playerA, "Scroll of Fate", 1); + addCard(Zone.HAND, playerA, "Basking Rootwalla", 1); // 1/1 + + // manifest + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Manifest"); + addTarget(playerA, "Basking Rootwalla"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } + + @Test + public void test_Simple_ManifestTargetPlayer() { + // Exile target creature. Its controller manifests the top card of their library. + addCard(Zone.HAND, playerA, "Reality Shift", 1); // {1}{U} + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + // + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears", 1); + + // manifest + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reality Shift"); + addTarget(playerA, "Grizzly Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + } + + @Test + public void test_Simple_ManifestFromOpponentLibrary() { + // At the beginning of each opponent's upkeep, you manifest the top card of that player's library. + addCard(Zone.BATTLEFIELD, playerA, "Thieving Amalgam", 1); + + // no drew on first turn, so use 5 turns to check same libs size at the end + runCode("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + Assert.assertEquals("libraries must be same on start", playerA.getLibrary().size(), playerB.getLibrary().size()); + }); + + // turn 1 + checkPermanentCount("turn 1.A - no face down", 1, PhaseStep.PRECOMBAT_MAIN, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + checkPermanentCount("turn 1.B - no face down", 1, PhaseStep.PRECOMBAT_MAIN, playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + + // turn 2 + checkPermanentCount("turn 2.A - +1 face down", 2, PhaseStep.PRECOMBAT_MAIN, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + checkPermanentCount("turn 2.B - no face down", 2, PhaseStep.PRECOMBAT_MAIN, playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + + // turn 3 + checkPermanentCount("turn 3.A - +1 face down", 3, PhaseStep.PRECOMBAT_MAIN, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); + checkPermanentCount("turn 3.B - no face down", 3, PhaseStep.PRECOMBAT_MAIN, playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + + // turn 4 + checkPermanentCount("turn 4.A - +2 face down", 4, PhaseStep.PRECOMBAT_MAIN, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2); + checkPermanentCount("turn 4.B - no face down", 4, PhaseStep.PRECOMBAT_MAIN, playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + + // turn 5 + checkPermanentCount("turn 5.A - +2 face down", 5, PhaseStep.PRECOMBAT_MAIN, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2); + checkPermanentCount("turn 5.B - no face down", 5, PhaseStep.PRECOMBAT_MAIN, playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 2); + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); + Assert.assertEquals("manifested cards must be taken from opponent's library", 2, playerA.getLibrary().size() - playerB.getLibrary().size()); + } + /** * Tests that ETB triggered abilities did not trigger for manifested cards */ @@ -540,6 +635,8 @@ public class ManifestTest extends CardTestPlayerBase { .filter(permanent -> permanent.isFaceDown(game)) .filter(permanent -> { Assert.assertEquals("face down permanent must have not name", "", permanent.getName()); + // TODO: buggy, manifested card must have some rules + //Assert.assertTrue("face down permanent must have abilities", permanent.getAbilities().size() > 0); return true; }) .findFirst() @@ -559,6 +656,8 @@ public class ManifestTest extends CardTestPlayerBase { .filter(p -> { CardView debugView = new CardView((PermanentCard) currentGame.getPermanent(p.getId()), currentGame, false, false); Assert.assertNotEquals("face down view must have name", "", p.getName()); + // TODO: buggy, manifested card must have some rules + //Assert.assertTrue("face down view must have abilities", p.getRules().size() > 0); return true; }) .findFirst() @@ -567,9 +666,9 @@ public class ManifestTest extends CardTestPlayerBase { return permanentView; } - private void assertFaceDown(String checkInfo, PermanentView faceDownPermanentView, String needRealName, String needFaceDownStatus, boolean needShowRealInfo) { + private void assertFaceDownManifest(String checkInfo, PermanentView faceDownPermanentView, String needRealName, boolean needShowRealInfo) { String info = checkInfo + " - " + faceDownPermanentView; - String needName = CardUtil.getCardNameForGUI(needShowRealInfo ? needRealName : "", needFaceDownStatus); + String needName = CardUtil.getCardNameForGUI(needShowRealInfo ? needRealName : "", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST); // check view Assert.assertTrue(info + " - wrong face down status", faceDownPermanentView.isFaceDown()); @@ -600,11 +699,11 @@ public class ManifestTest extends CardTestPlayerBase { runCode("on active game", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { // hide from opponent PermanentView permanent = findFaceDownPermanent(game, playerA, playerB); - assertFaceDown("in game: must hide from opponent", permanent, "Mountain", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, false); + assertFaceDownManifest("in game: must hide from opponent", permanent, "Mountain", false); // show for yourself permanent = findFaceDownPermanent(game, playerB, playerB); - assertFaceDown("in game: must show for yourself", permanent, "Mountain", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, true); + assertFaceDownManifest("in game: must show for yourself", permanent, "Mountain", true); }); setStrictChooseMode(true); @@ -617,9 +716,9 @@ public class ManifestTest extends CardTestPlayerBase { // show all after game end PermanentView permanent = findFaceDownPermanent(currentGame, playerA, playerB); - assertFaceDown("end game: must show for opponent", permanent, "Mountain", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, true); + assertFaceDownManifest("end game: must show for opponent", permanent, "Mountain", true); // permanent = findFaceDownPermanent(currentGame, playerB, playerB); - assertFaceDown("end game: must show for yourself", permanent, "Mountain", TokenRepository.XMAGE_IMAGE_NAME_FACE_DOWN_MANIFEST, true); + assertFaceDownManifest("end game: must show for yourself", permanent, "Mountain", true); } } 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 102b40239b4..ca8dc2dad67 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestEffect.java @@ -1,4 +1,3 @@ - package mage.abilities.effects.keyword; import mage.MageObjectReference; @@ -22,7 +21,36 @@ import mage.util.CardUtil; import java.util.Set; /** - * @author LevelX2 + * Manifest + *

+ * 701.34a + * To manifest a card, turn it face down. It becomes a 2/2 face-down creature card with no text, no name, no subtypes, + * and no mana cost. Put that card onto the battlefield face down. That permanent is a manifested permanent for as + * long as it remains face down. The effect defining its characteristics works while the card is face down and ends + * when it’s turned face up. + *

+ * 701.34b + * Any time you have priority, you may turn a manifested permanent you control face up. This is a special action + * that doesn’t use the stack (see rule 116.2b). To do this, show all players that the card representing that + * permanent is a creature card and what that card’s mana cost is, pay that cost, then turn the permanent face up. + * The effect defining its characteristics while it was face down ends, and it regains its normal characteristics. + * (If the card representing that permanent isn’t a creature card or it doesn’t have a mana cost, it can’t be turned + * face up this way.) + *

+ * 701.34c TODO: need support it + * If a card with morph is manifested, its controller may turn that card face up using either the procedure + * described in rule 702.37e to turn a face-down permanent with morph face up or the procedure described above + * to turn a manifested permanent face up. + *

+ * 701.34d TODO: need support it + * If a card with disguise is manifested, its controller may turn that card face up using either the procedure + * described in rule 702.168d to turn a face-down permanent with disguise face up or the procedure described + * above to turn a manifested permanent face up. + *

+ * 701.34e TODO: need support it + * If an effect instructs a player to manifest multiple cards from their library, those cards are manifested one at a time. + * + * @author LevelX2, JayDi85 */ public class ManifestEffect extends OneShotEffect { @@ -58,33 +86,58 @@ public class ManifestEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player controller = game.getPlayer(source.getControllerId()); - if (controller != null) { - Ability newSource = source.copy(); - newSource.setWorksFaceDown(true); - int value = amount.calculate(game, source, this); - Set cards = controller.getLibrary().getTopCards(game, value); - for (Card card : cards) { - ManaCosts manaCosts = null; - if (card.isCreature(game)) { - manaCosts = card.getSpellAbility() != null ? card.getSpellAbility().getManaCosts() : null; - if (manaCosts == null) { - manaCosts = new ManaCostsImpl<>("{0}"); - } - } - MageObjectReference objectReference = new MageObjectReference(card.getId(), card.getZoneChangeCounter(game) + 1, game); - game.addEffect(new BecomesFaceDownCreatureEffect(manaCosts, objectReference, Duration.Custom, FaceDownType.MANIFESTED), newSource); - - } - controller.moveCards(cards, Zone.BATTLEFIELD, source, game, false, true, false, null); - for (Card card : cards) { - Permanent permanent = game.getPermanent(card.getId()); - if (permanent != null) { - permanent.setManifested(true); - } - } - return true; + if (controller == null) { + return false; } - return false; + + int manifestAmount = amount.calculate(game, source, this); + return doManifestCards(game, source, controller, controller.getLibrary().getTopCards(game, manifestAmount)); + } + + public static boolean doManifestCards(Game game, Ability source, Player manifestPlayer, Set cardsToManifest) { + if (cardsToManifest.isEmpty()) { + return false; + } + + // prepare source ability + // TODO: looks buggy, must not change source ability! + // TODO: looks buggy, if target player manifested then source's controllerId will be wrong (not who manifested) + // so BecomesFaceDownCreatureEffect will see wrong source.controllerId + // (possible bugs: keep manifested after player leave/lose?) + Ability newSource = source.copy(); + newSource.setWorksFaceDown(true); + + // prepare face down effect for battlefield permanents + // TODO: need research - why it add effect before move?! + for (Card card : cardsToManifest) { + // 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 (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); + game.addEffect(new BecomesFaceDownCreatureEffect(manaCosts, objectReference, Duration.Custom, FaceDownType.MANIFESTED), newSource); + } + + // move cards to battlefield as face down + // 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()); + if (permanent != null) { + // TODO: why it set manifested here (face down effect doesn't work?!) + // 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 + } + } + + return true; } private String setText() { diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestTargetPlayerEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestTargetPlayerEffect.java index 23e0dfba3a8..599190c855f 100644 --- a/Mage/src/main/java/mage/abilities/effects/keyword/ManifestTargetPlayerEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/keyword/ManifestTargetPlayerEffect.java @@ -1,24 +1,12 @@ - package mage.abilities.effects.keyword; -import mage.MageObjectReference; import mage.abilities.Ability; -import mage.abilities.costs.mana.ManaCosts; -import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; -import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect; -import mage.abilities.effects.common.continuous.BecomesFaceDownCreatureEffect.FaceDownType; -import mage.cards.Card; -import mage.constants.Duration; import mage.constants.Outcome; -import mage.constants.Zone; import mage.game.Game; -import mage.game.permanent.Permanent; import mage.players.Player; import mage.util.CardUtil; -import java.util.Set; - /** * @author LevelX2 */ @@ -48,31 +36,11 @@ public class ManifestTargetPlayerEffect extends OneShotEffect { @Override public boolean apply(Game game, Ability source) { Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source)); - if (targetPlayer != null) { - Ability newSource = source.copy(); - newSource.setWorksFaceDown(true); - Set cards = targetPlayer.getLibrary().getTopCards(game, amount); - for (Card card : cards) { - ManaCosts manaCosts = null; - if (card.isCreature(game)) { - manaCosts = card.getSpellAbility() != null ? card.getSpellAbility().getManaCosts() : null; - if (manaCosts == null) { - manaCosts = new ManaCostsImpl<>("{0}"); - } - } - MageObjectReference objectReference = new MageObjectReference(card.getId(), card.getZoneChangeCounter(game) + 1, game); - game.addEffect(new BecomesFaceDownCreatureEffect(manaCosts, objectReference, Duration.Custom, FaceDownType.MANIFESTED), newSource); - } - targetPlayer.moveCards(cards, Zone.BATTLEFIELD, source, game, false, true, false, null); - for (Card card : cards) { - Permanent permanent = game.getPermanent(card.getId()); - if (permanent != null) { - permanent.setManifested(true); - } - } - return true; + if (targetPlayer == null) { + return false; } - return false; + + return ManifestEffect.doManifestCards(game, source, targetPlayer, targetPlayer.getLibrary().getTopCards(game, amount)); } private String setText() { diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 9b6959ebbe5..a6a723bca8c 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -791,14 +791,12 @@ public class Spell extends StackObjectImpl implements Card { @Override public boolean turnFaceUp(Ability source, Game game, UUID playerId) { - setFaceDown(false, game); - return true; + throw new IllegalStateException("Spells un-support turn face up commands"); } @Override public boolean turnFaceDown(Ability source, Game game, UUID playerId) { - setFaceDown(true, game); - return true; + throw new IllegalStateException("Spells un-support turn face up commands"); } @Override