From 6e1da09023eea715e5572246bef0e6fec9d2dc76 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Tue, 23 Jun 2020 00:59:17 +0400 Subject: [PATCH] * Morph ability - fixed that card with morph ability marked as playable all the time (#6680); --- .../cards/abilities/keywords/MorphTest.java | 72 ++++++++++++++++-- .../main/java/mage/abilities/AbilityImpl.java | 42 +++++----- .../main/java/mage/players/PlayerImpl.java | 76 +++++++++---------- 3 files changed, 119 insertions(+), 71 deletions(-) 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 253339652ef..7e3333567aa 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 @@ -636,15 +636,14 @@ public class MorphTest extends CardTestPlayerBase { * restriction." */ @Test - public void testReflectorMageBouncesFaceupCreatureReplayAsMorph() { - + public void test_ReflectorMageCantStopMorphToCast_TryNormalCast() { // {1}{W}{U} When Reflector Mage enters the battlefield, return target creature an opponent controls to its owner's hand. // That creature's owner can't cast spells with the same name as that creature until your next turn. addCard(Zone.HAND, playerA, "Reflector Mage"); // 2/3 addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); addCard(Zone.BATTLEFIELD, playerA, "Island", 2); - //Tap: Add {G}, {U}, or {R}. + // Tap: Add {G}, {U}, or {R}. // Morph 2 (You may cast this card face down as a 2/2 creature for 3. Turn it face up any time for its morph cost.) // When Rattleclaw Mystic is turned face up, add {G}{U}{R}. addCard(Zone.BATTLEFIELD, playerB, "Rattleclaw Mystic"); // 2/1 @@ -652,20 +651,58 @@ public class MorphTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Island"); addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + // return to hand castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reflector Mage"); addTarget(playerA, "Rattleclaw Mystic"); + // try cast as normal -- must not work castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Rattleclaw Mystic"); - setChoice(playerB, "Yes"); // cast it face down as 2/2 creature + setChoice(playerB, "No"); // try cast as normal + //setStrictChooseMode(true); // no strict mode - cause can't cast as normal setStopAt(2, PhaseStep.BEGIN_COMBAT); - execute(); + //assertAllCommandsUsed(); assertPermanentCount(playerA, "Reflector Mage", 1); assertPermanentCount(playerB, "Rattleclaw Mystic", 0); - assertHandCount(playerB, "Rattleclaw Mystic", 0); // should have been replayed - assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); // Rattleclaw played as a morph + assertHandCount(playerB, "Rattleclaw Mystic", 1); // can't play + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 0); // don't try as morph + } + + @Test + public void test_ReflectorMageCantStopMorphToCast_TryMorph() { + // {1}{W}{U} When Reflector Mage enters the battlefield, return target creature an opponent controls to its owner's hand. + // That creature's owner can't cast spells with the same name as that creature until your next turn. + addCard(Zone.HAND, playerA, "Reflector Mage"); // 2/3 + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + // Tap: Add {G}, {U}, or {R}. + // Morph 2 (You may cast this card face down as a 2/2 creature for 3. Turn it face up any time for its morph cost.) + // When Rattleclaw Mystic is turned face up, add {G}{U}{R}. + addCard(Zone.BATTLEFIELD, playerB, "Rattleclaw Mystic"); // 2/1 + addCard(Zone.BATTLEFIELD, playerB, "Forest"); + addCard(Zone.BATTLEFIELD, playerB, "Island"); + addCard(Zone.BATTLEFIELD, playerB, "Mountain"); + + // return to hand + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Reflector Mage"); + addTarget(playerA, "Rattleclaw Mystic"); + + // try cast as morph - must work + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Rattleclaw Mystic"); + setChoice(playerB, "Yes"); // try cast as morph + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Reflector Mage", 1); + assertPermanentCount(playerB, "Rattleclaw Mystic", 0); + assertHandCount(playerB, "Rattleclaw Mystic", 0); // able cast as morph + assertPermanentCount(playerB, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); } /** @@ -1007,4 +1044,25 @@ public class MorphTest extends CardTestPlayerBase { assertPermanentCount(playerA, "Zoetic Cavern", 0); assertPermanentCount(playerA, EmptyNames.FACE_DOWN_CREATURE.toString(), 1); } + + @Test + public void test_CantActivateOnOpponentTurn() { + // https://github.com/magefree/mage/issues/6698 + + // Morph {1}{U} (You may cast this card face down as a 2/2 creature for {3}. Turn it face up any time for its morph cost.) + // When Willbender is turned face up, change the target of target spell or ability with a single target. + addCard(Zone.HAND, playerA, "Willbender"); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + + // can play on own turn + checkPlayableAbility("can", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Willbender", true); + + // can't play on opponent turn + checkPlayableAbility("can't", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Willbender", false); + + setStrictChooseMode(true); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + } } diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 62a2d3fea1f..4a053e6a861 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -425,36 +425,28 @@ public abstract class AbilityImpl implements Ability { } } - boolean alternativeCostisUsed = false; + boolean alternativeCostUsed = false; if (sourceObject != null && !(sourceObject instanceof Permanent)) { - Abilities abilities = null; - if (sourceObject instanceof Card) { - abilities = ((Card) sourceObject).getAbilities(game); - } else { - sourceObject.getAbilities(); - } - - if (abilities != null) { - for (Ability ability : abilities) { - // if cast for noMana no Alternative costs are allowed - if (canUseAlternativeCost && !noMana && ability instanceof AlternativeSourceCosts) { - AlternativeSourceCosts alternativeSpellCosts = (AlternativeSourceCosts) ability; - if (alternativeSpellCosts.isAvailable(this, game)) { - if (alternativeSpellCosts.askToActivateAlternativeCosts(this, game)) { - // only one alternative costs may be activated - alternativeCostisUsed = true; - break; - } + Abilities abilities = CardUtil.getAbilities(sourceObject, game); + for (Ability ability : abilities) { + // if cast for noMana no Alternative costs are allowed + if (canUseAlternativeCost && !noMana && ability instanceof AlternativeSourceCosts) { + AlternativeSourceCosts alternativeSpellCosts = (AlternativeSourceCosts) ability; + if (alternativeSpellCosts.isAvailable(this, game)) { + if (alternativeSpellCosts.askToActivateAlternativeCosts(this, game)) { + // only one alternative costs may be activated + alternativeCostUsed = true; + break; } } - if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) { - ((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game); - } + } + if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) { + ((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game); } } // controller specific alternate spell costs - if (canUseAlternativeCost && !noMana && !alternativeCostisUsed) { + if (canUseAlternativeCost && !noMana && !alternativeCostUsed) { if (this.getAbilityType() == AbilityType.SPELL // 117.9a Only one alternative cost can be applied to any one spell as it's being cast. // So an alternate spell ability can't be paid with Omniscience @@ -463,7 +455,7 @@ public abstract class AbilityImpl implements Ability { if (alternativeSourceCosts.isAvailable(this, game)) { if (alternativeSourceCosts.askToActivateAlternativeCosts(this, game)) { // only one alternative costs may be activated - alternativeCostisUsed = true; + alternativeCostUsed = true; break; } } @@ -472,7 +464,7 @@ public abstract class AbilityImpl implements Ability { } } - return alternativeCostisUsed; + return alternativeCostUsed; } /** diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 5ac203e08ed..d2066015eef 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3140,54 +3140,52 @@ public abstract class PlayerImpl implements Player, Serializable { } protected ActivatedAbility findActivatedAbilityFromAlternativeSourceCost(MageObject object, ManaOptions manaAvailable, Ability ability, Game game) { - // alternative cost must be replaced by real play ability - if (ability instanceof AlternativeSourceCosts) { - AlternativeSourceCosts altAbility = (AlternativeSourceCosts) ability; + // return play ability that can activate AlternativeSourceCosts + if (ability instanceof AlternativeSourceCosts && !(object instanceof Permanent)) { + ActivatedAbility playAbility = null; if (object.isLand()) { - // land - // morph ability is static, so it must be replaced with play land ability (playLand search and try to use face down first) - if (canLandPlayAlternateSourceCostsAbility(object, manaAvailable, ability, game)) { // e.g. Land with Morph - Ability landAbility = CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); - if (landAbility != null) { - return (PlayLandAbility) landAbility; - } - } + playAbility = (PlayLandAbility) CardUtil.getAbilities(object, game).stream().filter(a -> a instanceof PlayLandAbility).findFirst().orElse(null); + } else if (object instanceof Card) { + playAbility = ((Card) object).getSpellAbility(); + } + if (playAbility == null) { + return null; + } + + // 707.4.Objects that are cast face down are turned face down before they are put onto the stack + // (e.g. no lands per turn limit, no cast restrictions, another cost, etc) + // so morph must checks only mana payment here + boolean canUse; + if (ability instanceof MorphAbility) { + canUse = game.canPlaySorcery(playerId) && canPayAlternateSourceCostsAbility(object, playAbility, manaAvailable, ability, game); } else { - // creature and other - if (object instanceof Card) { - SpellAbility spellAbility = ((Card) object).getSpellAbility(); - if (altAbility.isAvailable(spellAbility, game)) { - return spellAbility; - } - } + canUse = canPlay(playAbility, manaAvailable, object, game); // canPlay already checks alternative source costs and all conditions + } + + if (canUse) { + return playAbility; } } return null; } - protected boolean canLandPlayAlternateSourceCostsAbility(MageObject sourceObject, ManaOptions available, Ability ability, Game game) { - if (sourceObject != null && !(sourceObject instanceof Permanent)) { - Ability sourceAbility = sourceObject.getAbilities().stream() - .filter(landAbility -> landAbility.getAbilityType() == AbilityType.PLAY_LAND) - .findFirst().orElse(null); - - if (sourceAbility != null && ((AlternativeSourceCosts) ability).isAvailable(sourceAbility, game)) { - if (ability.getCosts().canPay(ability, sourceObject.getId(), this.getId(), game)) { - ManaCostsImpl manaCosts = new ManaCostsImpl(); - for (Cost cost : ability.getCosts()) { - if (cost instanceof ManaCost) { - manaCosts.add((ManaCost) cost); - } + protected boolean canPayAlternateSourceCostsAbility(MageObject sourceObject, Ability sourceAbility, ManaOptions available, Ability alternativeAbility, Game game) { + if (sourceAbility != null && ((AlternativeSourceCosts) alternativeAbility).isAvailable(sourceAbility, game)) { + if (alternativeAbility.getCosts().canPay(alternativeAbility, sourceObject.getId(), this.getId(), game)) { + ManaCostsImpl manaCosts = new ManaCostsImpl(); + for (Cost cost : alternativeAbility.getCosts()) { + if (cost instanceof ManaCost) { + manaCosts.add((ManaCost) cost); } + } - if (manaCosts.isEmpty()) { - return true; - } else { - for (Mana mana : manaCosts.getOptions()) { - for (Mana avail : available) { - if (mana.enough(avail)) { - return true; - } + if (manaCosts.isEmpty()) { + return true; + } else { + for (Mana mana : manaCosts.getOptions()) { + for (Mana avail : available) { + if (mana.enough(avail)) { + return true; } } }