diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java index 9f58d3b462b..6d0652df666 100644 --- a/Mage.Common/src/main/java/mage/view/CardView.java +++ b/Mage.Common/src/main/java/mage/view/CardView.java @@ -373,14 +373,6 @@ public class CardView extends SimpleCardView { this.superTypes = new ArrayList<>(card.getSuperType()); this.subTypes = card.getSubtype().copy(); this.rules = new ArrayList<>(card.getRules()); - - // additional rules for stack (example: morph ability text) - if (sourceCard instanceof Spell) { - List extraRules = sourceCard.getSpellAbility().getSpellAbilityCastMode().getAdditionalRulesOnStack(); - if (extraRules != null) { - this.rules.addAll(extraRules); - } - } } // GUI: enable day/night button to view original face up card diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisguiseTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisguiseTest.java index 2740edf703a..5befc0cfd8e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisguiseTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/DisguiseTest.java @@ -1,11 +1,14 @@ package org.mage.test.cards.abilities.keywords; import mage.abilities.Ability; +import mage.abilities.SpellAbility; +import mage.abilities.common.TurnFaceUpAbility; import mage.abilities.common.TurnedFaceUpSourceTriggeredAbility; import mage.constants.EmptyNames; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.game.permanent.Permanent; +import mage.view.CardView; import mage.view.GameView; import mage.view.PermanentView; import mage.view.PlayerView; @@ -13,6 +16,8 @@ import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; +import java.util.List; + /** * Most of the face down logic was tested in MorphTest, here are tests for disguise related only * @@ -21,7 +26,17 @@ import org.mage.test.serverside.base.CardTestPlayerBase; public class DisguiseTest extends CardTestPlayerBase { @Test - public void test_NormalPlayAndClientData() { + public void test_NormalPlay_ClientData_CostRulesVisible() { + // it checks rules visible for face down cards, main logic: + // - real face up abilities uses special cost; + // - it must be hidden from opponent + // - so it must be replaced in rules by non-cost versions (e.g. text only) + + String FACE_DOWN_SPELL = "with no text, no name, no subtypes"; + String FACE_DOWN_TRIGGER = "When "; + String FACE_DOWN_FACE_UP = "down permanent face up"; + + // {R}{W} // Disguise {R/W}{R/W} (You may cast this card face down for {3} as a 2/2 creature with ward {2}. // Turn it face up any time for its disguise cost.) @@ -38,6 +53,18 @@ public class DisguiseTest extends CardTestPlayerBase { // prepare face down castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dog Walker using Disguise"); + runCode("face up on stack", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + Assert.assertEquals("stack, server - can't find spell", 1, currentGame.getStack().size()); + SpellAbility spellAbility = (SpellAbility) currentGame.getStack().getFirst().getStackAbility(); + Assert.assertEquals("stack, server - can't find spell", "Cast Dog Walker using Disguise", spellAbility.getName()); + CardView spellView = getGameView(playerA).getStack().values().stream().findFirst().orElse(null); + Assert.assertNotNull("stack, client: can't find spell", spellView); + + // make sure rules visible + assertRuleExist("client side, stack: face down spell - show", spellView.getRules(), FACE_DOWN_SPELL, true); + assertRuleExist("client side, stack: face up - hide", spellView.getRules(), FACE_DOWN_FACE_UP, false); + assertRuleExist("client side, stack: triggered ability - hide", spellView.getRules(), FACE_DOWN_TRIGGER, false); + }); waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); checkPermanentCount("after face down", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dog Walker", 0); checkPermanentCount("after face down", 1, PhaseStep.PRECOMBAT_MAIN, playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); @@ -53,9 +80,18 @@ public class DisguiseTest extends CardTestPlayerBase { Assert.assertEquals("server side: wrong color", "", permanent.getColor(currentGame).toString()); Assert.assertEquals("server side: wrong power", "2", permanent.getPower().toString()); Assert.assertEquals("server side: wrong toughness", "2", permanent.getToughness().toString()); + + // make sure real abilities exists + // trigger Ability ability = permanent.getAbilities(currentGame).stream().filter(a -> a instanceof TurnedFaceUpSourceTriggeredAbility).findFirst().orElse(null); Assert.assertNotNull("server side: must have face up triggered ability", ability); Assert.assertFalse("server side: face up triggered ability must be hidden", ability.getRuleVisible()); + // face up + ability = permanent.getAbilities(currentGame).stream().filter(a -> a instanceof TurnFaceUpAbility).findFirst().orElse(null); + Assert.assertNotNull("server side: must have turn face up ability", ability); + String foundRule = permanent.getRules(currentGame).stream().filter(r -> r.contains("{R/W}")).findFirst().orElse(null); + // failed here? search BecomesFaceDownCreatureEffect and additionalAbilities + Assert.assertNull("server side: turn face up ability with {R/W} cost must be replaced by text only without cost", foundRule); // client side - controller GameView gameView = getGameView(playerA); @@ -69,9 +105,11 @@ public class DisguiseTest extends CardTestPlayerBase { Assert.assertEquals("client side - controller: wrong color", "", permanentView.getColor().toString()); Assert.assertEquals("client side - controller: wrong power", "2", permanentView.getPower()); Assert.assertEquals("client side - controller: wrong toughness", "2", permanentView.getToughness()); - // make sure rules hiding works fine - Assert.assertTrue("client side - controller: face up triggered ability must be hidden", - permanentView.getRules().stream().noneMatch(r -> r.contains("When "))); + // make sure rules visible + assertRuleExist("client side, controller: face down spell - show", permanentView.getRules(), FACE_DOWN_SPELL, true); + assertRuleExist("client side, controller: face up - hide", permanentView.getRules(), FACE_DOWN_FACE_UP, false); + assertRuleExist("client side, controller: triggered ability - hide", permanentView.getRules(), FACE_DOWN_TRIGGER, false); + assertRuleExist("client side, controller: {R/W} cost hide", permanentView.getRules(), "{R/W}", false); // client side - opponent gameView = getGameView(playerB); @@ -91,9 +129,11 @@ public class DisguiseTest extends CardTestPlayerBase { Assert.assertEquals("client side - opponent: wrong color", "", permanentView.getColor().toString()); Assert.assertEquals("client side - opponent: wrong power", "2", permanentView.getPower()); Assert.assertEquals("client side - opponent: wrong toughness", "2", permanentView.getToughness()); - // make sure rules hiding works fine - Assert.assertTrue("client side - opponent: face up triggered ability must be hidden", - permanentView.getRules().stream().noneMatch(r -> r.contains("When "))); + // make sure rules visible + assertRuleExist("client side, opponent: face down spell - show", permanentView.getRules(), FACE_DOWN_SPELL, true); + assertRuleExist("client side, opponent: face up - hide", permanentView.getRules(), FACE_DOWN_FACE_UP, false); + assertRuleExist("client side, opponent: triggered ability - hide", permanentView.getRules(), FACE_DOWN_TRIGGER, false); + assertRuleExist("client side, opponent: {R/W} cost hide", permanentView.getRules(), "{R/W}", false); }); // make sure ward works too @@ -124,4 +164,13 @@ public class DisguiseTest extends CardTestPlayerBase { setStopAt(1, PhaseStep.END_TURN); execute(); } + + private void assertRuleExist(String info, List rules, String searchPart, boolean mustExists) { + String foundAbility = rules.stream().filter(r -> r.contains(searchPart)).findFirst().orElse(null); + if (mustExists) { + Assert.assertTrue(info, foundAbility != null); + } else { + Assert.assertFalse(info, foundAbility != null); + } + } } 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 4de4011c619..2c2837c3043 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 @@ -13,10 +13,12 @@ import org.mage.test.player.TestPlayer; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * @author levelX2 + * @author levelX2, JayDi85 */ public class MorphTest extends CardTestPlayerBase { + // DisguiseTest contains additional rules generation tests for face down + /** * Tests if a creature with Morph is cast normal, it behaves as normal * creature diff --git a/Mage/src/main/java/mage/abilities/common/TurnFaceUpAbility.java b/Mage/src/main/java/mage/abilities/common/TurnFaceUpAbility.java index c49bcc857b2..d4975fb0d4f 100644 --- a/Mage/src/main/java/mage/abilities/common/TurnFaceUpAbility.java +++ b/Mage/src/main/java/mage/abilities/common/TurnFaceUpAbility.java @@ -29,8 +29,8 @@ public class TurnFaceUpAbility extends SpecialAction { this.addCost(costs); this.usesStack = false; this.abilityType = AbilityType.SPECIAL_ACTION; - this.setRuleVisible(false); // will be made visible only to controller in CardView this.setWorksFaceDown(true); + this.setRuleVisible(false); // hide in face up, but show in face down view (it will be enabled as default ability) } protected TurnFaceUpAbility(final TurnFaceUpAbility ability) { diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureAllEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureAllEffect.java index 909e3ee263c..73a24776138 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureAllEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/BecomesFaceDownCreatureAllEffect.java @@ -15,6 +15,8 @@ import mage.game.permanent.Permanent; import java.util.*; /** + * TODO: must be reworked to use same face down logic as BecomesFaceDownCreatureEffect + * * @author LevelX2 */ 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 d109f0e7b24..fae5ada5d09 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 @@ -4,12 +4,14 @@ import mage.MageObject; import mage.MageObjectReference; import mage.ObjectColor; import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.TurnFaceUpAbility; import mage.abilities.costs.Cost; import mage.abilities.costs.Costs; import mage.abilities.costs.CostsImpl; import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.InfoEffect; import mage.abilities.keyword.WardAbility; import mage.cards.Card; import mage.cards.CardImpl; @@ -29,6 +31,7 @@ import java.util.UUID; /** * Support different face down types: morph/manifest and disguise/cloak + * *

* This effect lets the card be a 2/2 face-down creature, with no text, no name, * no subtypes, and no mana cost, if it's face down on the battlefield. And it @@ -81,15 +84,36 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl { this.objectReference = objectReference; this.zoneChangeCounter = Integer.MIN_VALUE; - // additional abilities - // face up + // add additional face up and information abilities if (turnFaceUpCosts != null) { + // face up for all this.additionalAbilities.add(new TurnFaceUpAbility(turnFaceUpCosts, faceDownType == FaceDownType.MEGAMORPHED)); - } - // ward - if (faceDownType == FaceDownType.DISGUISED - || faceDownType == faceDownType.CLOAKED) { - this.additionalAbilities.add(new WardAbility(new ManaCostsImpl<>("{2}"))); + + switch (faceDownType) { + case MORPHED: + case MEGAMORPHED: + // face up rules replace for cost hide + this.additionalAbilities.add(new SimpleStaticAbility(Zone.ALL, new InfoEffect( + "Turn it face up any time for its morph cost." + ))); + break; + case DISGUISED: + case CLOAKED: + // ward + this.additionalAbilities.add(new WardAbility(new ManaCostsImpl<>("{2}"))); + + // face up rules replace for cost hide + this.additionalAbilities.add(new SimpleStaticAbility(Zone.ALL, new InfoEffect( + "Turn it face up any time for its disguise/cloaked cost." + ))); + break; + case MANUAL: + case MANIFESTED: + // no face up abilities + break; + default: + throw new IllegalArgumentException("Un-supported face down type: " + faceDownType); + } } staticText = "{this} becomes a 2/2 face-down creature, with no text, no name, no subtypes, and no mana cost"; @@ -216,16 +240,17 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl { // If a card with morph is manifested, its controller may turn that card face up using // either the procedure described in rule 702.36e to turn a face-down permanent with morph face up // or the procedure described above to turn a manifested permanent face up. - // - // so keep all tune face up abilities and other face down compatible + + // keep face down abilities active, but hide it from rules description if (ability.getWorksFaceDown()) { - // keep face down abilities active, but hide it from rules description + // example: When Dog Walker is turned face up, create two tapped 1/1 white Dog creature tokens ability.setRuleVisible(false); - // but do not hide default ability (becomes a 2/2 face-down creature) - if (!ability.getRuleVisible() && !ability.getEffects().isEmpty()) { + // becomes a 2/2 face-down creature - it hides a real ability too, but adds fake rule, see + if (!ability.getEffects().isEmpty()) { if (ability.getEffects().get(0) instanceof BecomesFaceDownCreatureEffect) { + // enable for stack ability.setRuleVisible(true); } } @@ -236,7 +261,7 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl { abilitiesToRemove.add(ability); } - // add additional abilities like face up + // add additional abilities like face up (real ability hidden and duplicated with information without cost data) if (object instanceof Permanent) { // as permanent Permanent permanentObject = (Permanent) object; @@ -244,7 +269,7 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl { if (additionalAbilities != null) { additionalAbilities.forEach(blueprintAbility -> { Ability newAbility = blueprintAbility.copy(); - newAbility.setRuleVisible(true); + newAbility.setRuleVisible(CardUtil.isInformationAbility(newAbility)); permanentObject.addAbility(newAbility, sourceId, game); }); } @@ -255,7 +280,7 @@ public class BecomesFaceDownCreatureEffect extends ContinuousEffectImpl { if (additionalAbilities != null) { additionalAbilities.forEach(blueprintAbility -> { Ability newAbility = blueprintAbility.copy(); - newAbility.setRuleVisible(true); + newAbility.setRuleVisible(CardUtil.isInformationAbility(newAbility)); cardObject.addAbility(newAbility); }); } diff --git a/Mage/src/main/java/mage/abilities/keyword/DisguiseAbility.java b/Mage/src/main/java/mage/abilities/keyword/DisguiseAbility.java index 7c82842bbd9..b93d5e313fd 100644 --- a/Mage/src/main/java/mage/abilities/keyword/DisguiseAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/DisguiseAbility.java @@ -100,9 +100,9 @@ public class DisguiseAbility extends SpellAbility { @Override public String getRule() { boolean isMana = disguiseCosts.get(0) instanceof ManaCost; + String costInfo = this.disguiseCosts.getText() + (isMana ? " " : ". "); return ABILITY_KEYWORD + (isMana ? " " : "—") - + this.disguiseCosts.getText() - + (isMana ? ' ' : ". ") + + costInfo + " (" + REMINDER_TEXT + ")"; } } diff --git a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java index 25d7a7b6634..d36e91e50a9 100644 --- a/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java +++ b/Mage/src/main/java/mage/constants/SpellAbilityCastMode.java @@ -8,9 +8,6 @@ import mage.cards.Card; import mage.game.Game; import mage.game.stack.Spell; -import java.util.Collections; -import java.util.List; - /** * @author LevelX2 */ @@ -20,18 +17,13 @@ public enum SpellAbilityCastMode { FLASHBACK("Flashback"), BESTOW("Bestow"), PROTOTYPE("Prototype"), - MORPH("Morph", false, true, SpellAbilityCastMode.MORPH_ADDITIONAL_RULE), - MEGAMORPH("Megamorph", false, true, SpellAbilityCastMode.MORPH_ADDITIONAL_RULE), - DISGUISE("Disguise", false, true, SpellAbilityCastMode.DISGUISE_ADDITIONAL_RULE), + MORPH("Morph", false, true), + MEGAMORPH("Megamorph", false, true), + DISGUISE("Disguise", false, true), TRANSFORMED("Transformed", true), DISTURB("Disturb", true), MORE_THAN_MEETS_THE_EYE("More than Meets the Eye", true); - private static final String MORPH_ADDITIONAL_RULE = "You may cast this card as a 2/2 face-down creature, with no text," - + " no name, no subtypes, and no mana cost by paying {3} rather than paying its mana cost."; - private static final String DISGUISE_ADDITIONAL_RULE = "You may cast this card face down for {3} as a 2/2 creature with " - + "ward {2}. Turn it face up any time for its disguise cost."; - private final String text; // should the cast mode use the second face? @@ -39,10 +31,6 @@ public enum SpellAbilityCastMode { private final boolean isFaceDown; - // use it to add additional info in stack object cause face down has nothing - // TODO: is it possible to use InfoEffect or CardHint instead that? - private final List additionalRulesOnStack; - public boolean isTransformed() { return this.isTransformed; } @@ -52,24 +40,19 @@ public enum SpellAbilityCastMode { } SpellAbilityCastMode(String text, boolean isTransformed) { - this(text, isTransformed, false, null); + this(text, isTransformed, false); } - SpellAbilityCastMode(String text, boolean isTransformed, boolean isFaceDown, String additionalRulesOnStack) { + SpellAbilityCastMode(String text, boolean isTransformed, boolean isFaceDown) { this.text = text; this.isTransformed = isTransformed; this.isFaceDown = isFaceDown; - this.additionalRulesOnStack = additionalRulesOnStack == null ? null : Collections.singletonList(additionalRulesOnStack); } public boolean isFaceDown() { return this.isFaceDown; } - public List getAdditionalRulesOnStack() { - return additionalRulesOnStack; - } - @Override public String toString() { return text; diff --git a/Mage/src/main/java/mage/filter/predicate/mageobject/NoAbilityPredicate.java b/Mage/src/main/java/mage/filter/predicate/mageobject/NoAbilityPredicate.java index fe450dc5a35..4fa84f22cd0 100644 --- a/Mage/src/main/java/mage/filter/predicate/mageobject/NoAbilityPredicate.java +++ b/Mage/src/main/java/mage/filter/predicate/mageobject/NoAbilityPredicate.java @@ -8,6 +8,7 @@ import mage.abilities.keyword.special.JohanVigilanceAbility; import mage.cards.Card; import mage.filter.predicate.Predicate; import mage.game.Game; +import mage.util.CardUtil; import java.util.Objects; @@ -43,11 +44,18 @@ public enum NoAbilityPredicate implements Predicate { // (2007-05-01) for (Ability ability : abilities) { + // ignore inner face down abilities like turn up and becomes creature if (ability.getWorksFaceDown()) { - // inner face down abilities like turn up and becomes creature continue; } - if (!Objects.equals(ability.getClass(), SpellAbility.class) && !ability.getClass().equals(JohanVigilanceAbility.class)) { + + // ignore information abilities + if (CardUtil.isInformationAbility(ability)) { + continue; + } + + if (!Objects.equals(ability.getClass(), SpellAbility.class) + && !ability.getClass().equals(JohanVigilanceAbility.class)) { return false; } } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 1b2aa5ddc78..fe884e5b028 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -13,6 +13,7 @@ import mage.abilities.dynamicvalue.common.SavedDamageValue; import mage.abilities.dynamicvalue.common.StaticValue; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; +import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.asthought.CanPlayCardControllerEffect; import mage.abilities.effects.common.asthought.YouMaySpendManaAsAnyColorToCastTargetEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; @@ -2237,4 +2238,12 @@ public final class CardUtil { public static boolean canShowAsControlled(Card card, UUID createdForPlayer) { return card.getControllerOrOwnerId().equals(createdForPlayer); } + + /** + * Ability used for information only, e.g. adds additional rule texts + */ + public static boolean isInformationAbility(Ability ability) { + return !ability.getEffects().isEmpty() + && ability.getEffects().stream().allMatch(e -> e instanceof InfoEffect); + } }