From b38ac2f575c3da161f98ac3fdc2048c9fc446169 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Wed, 10 Jun 2020 08:28:18 +0400 Subject: [PATCH] * Split cards - added cost modification effects support for fused spells (#227, #2242, #6603, #6549); --- .../cards/abilities/keywords/BestowTest.java | 14 +- .../TappedForManaFromMultipleEffects.java | 4 +- ...astSplitCardsWithCostModificationTest.java | 122 +++++++++++++++++- .../java/org/mage/test/player/TestPlayer.java | 12 +- Mage/src/main/java/mage/game/stack/Spell.java | 34 ++--- .../main/java/mage/players/PlayerImpl.java | 8 +- 6 files changed, 149 insertions(+), 45 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BestowTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BestowTest.java index 5427cb84607..6dd7e5e3974 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BestowTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/BestowTest.java @@ -11,7 +11,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class BestowTest extends CardTestPlayerBase { @@ -20,7 +19,6 @@ public class BestowTest extends CardTestPlayerBase { * Tests that if from bestow permanent targeted creature gets protection * from the color of the bestow permanent, the bestow permanent becomes a * creature on the battlefield. - * */ /* Silent Artisan @@ -157,7 +155,7 @@ public class BestowTest extends CardTestPlayerBase { * // Away casting both sides, will the creature that has bestow come in * time for it to be sacrificed or does it fully resolve before the creature * comes in? - * + *

* Bestowed creature can be used to sacrifice a creature for the Away part. * http://www.mtgsalvation.com/forums/magic-fundamentals/magic-rulings/magic-rulings-archives/513828-bestow-far-away */ @@ -188,12 +186,16 @@ public class BestowTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nyxborn Rollicker using bestow", "Cyclops of One-Eyed Pass"); - castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "fused Far // Away", "Cyclops of One-Eyed Pass"); - addTarget(playerB, playerA); + castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "fused Far // Away"); + addTarget(playerB, "Cyclops of One-Eyed Pass"); // Far + addTarget(playerB, playerA); // Away addTarget(playerA, "Nyxborn Rollicker"); + + setStrictChooseMode(true); setStopAt(1, PhaseStep.END_TURN); execute(); + assertAllCommandsUsed(); assertHandCount(playerA, "Cyclops of One-Eyed Pass", 1); assertHandCount(playerB, 0); @@ -246,8 +248,6 @@ public class BestowTest extends CardTestPlayerBase { } /** - * - * * */ @Test diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/TappedForManaFromMultipleEffects.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/TappedForManaFromMultipleEffects.java index e8927641047..91f3f82a049 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/TappedForManaFromMultipleEffects.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/TappedForManaFromMultipleEffects.java @@ -55,8 +55,8 @@ public class TappedForManaFromMultipleEffects extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Nyxbloom Ancient"); // TODO: TAPPED_FOR_MANA replace event called from checkTappedForManaReplacement and start to choose replace events (is that problem?) // use case (that test): comment one 1-2 choices to fail in 1-2 calls - setChoice(playerA, "Nyxbloom Ancient: If you tap a permanent"); // getPlayable... checkTappedForManaReplacement... chooseReplacementEffect - setChoice(playerA, "Nyxbloom Ancient: If you tap a permanent"); // playManaAbility... resolve... checkToFirePossibleEvents... chooseReplacementEffect + setChoice(playerA, "Nyxbloom Ancient"); // getPlayable... checkTappedForManaReplacement... chooseReplacementEffect + setChoice(playerA, "Nyxbloom Ancient"); // playManaAbility... resolve... checkToFirePossibleEvents... chooseReplacementEffect // cast chloro castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Chlorophant"); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithCostModificationTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithCostModificationTest.java index ae6f1a97d64..0e01666d15a 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithCostModificationTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/split/CastSplitCardsWithCostModificationTest.java @@ -1,8 +1,11 @@ package org.mage.test.cards.split; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.cost.SpellsCostReductionAllEffect; import mage.constants.PhaseStep; import mage.constants.Zone; -import org.junit.Ignore; +import mage.filter.FilterCard; +import mage.filter.predicate.mageobject.NamePredicate; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; @@ -11,6 +14,118 @@ import org.mage.test.serverside.base.CardTestPlayerBase; */ public class CastSplitCardsWithCostModificationTest extends CardTestPlayerBase { + private void prepareReduceEffect(String cardNameToReduce, int reduceAmount) { + FilterCard filter = new FilterCard(); + filter.add(new NamePredicate(cardNameToReduce)); + addCustomCardWithAbility("reduce", playerA, new SimpleStaticAbility( + new SpellsCostReductionAllEffect(filter, reduceAmount)) + ); + } + + @Test + public void test_Playable_Left() { + // cost reduce for easy test + prepareReduceEffect("Armed", 3); + + // Armed {1}{R} Target creature gets +1/+1 and gains double strike until end of turn. + // Dangerous {3}{G} All creatures able to block target creature this turn do so. + addCard(Zone.HAND, playerA, "Armed // Dangerous", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + //addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Armed", true); + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Dangerous", false); + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast fused Armed // Dangerous", false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Armed", "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Armed // Dangerous", 1); + } + + @Test + public void test_Playable_Right() { + // cost reduce for easy test + prepareReduceEffect("Dangerous", 3); + + // Armed {1}{R} Target creature gets +1/+1 and gains double strike until end of turn. + // Dangerous {3}{G} All creatures able to block target creature this turn do so. + addCard(Zone.HAND, playerA, "Armed // Dangerous", 1); + //addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Armed", false); + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Dangerous", true); + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast fused Armed // Dangerous", false); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Dangerous", "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Armed // Dangerous", 1); + } + + @Test + public void test_Playable_Fused_Left() { + // cost reduce for easy test + prepareReduceEffect("Armed", 4); + + // Armed {1}{R} Target creature gets +1/+1 and gains double strike until end of turn. + // Dangerous {3}{G} All creatures able to block target creature this turn do so. + addCard(Zone.HAND, playerA, "Armed // Dangerous", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Armed", true); + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Dangerous", false); + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast fused Armed // Dangerous", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "fused Armed // Dangerous"); + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Armed // Dangerous", 1); + } + + @Test + public void test_Playable_Fused_Right() { + // cost reduce for easy test + prepareReduceEffect("Dangerous", 4); + + // Armed {1}{R} Target creature gets +1/+1 and gains double strike until end of turn. + // Dangerous {3}{G} All creatures able to block target creature this turn do so. + addCard(Zone.HAND, playerA, "Armed // Dangerous", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 1); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); + + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Armed", true); // no reduced, but have basic lands ({G}{R}) + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Dangerous", true); + checkPlayableAbility("all", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast fused Armed // Dangerous", true); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "fused Armed // Dangerous"); + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + assertAllCommandsUsed(); + + assertGraveyardCount(playerA, "Armed // Dangerous", 1); + } + @Test public void test_CostReduction_Simple() { // {2}{W}{U} @@ -26,6 +141,8 @@ public class CastSplitCardsWithCostModificationTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // cast Council + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {W}", 3); + activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 1); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Council of the Absolute"); setChoice(playerA, "Blastfire Bolt"); @@ -69,7 +186,6 @@ public class CastSplitCardsWithCostModificationTest extends CardTestPlayerBase { checkPlayableAbility("after reduction", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Armed", true); checkPlayableAbility("after reduction", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Dangerous", false); checkPlayableAbility("after reduction", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast fused Armed // Dangerous", false); - showAvaileableAbilities("after", 1, PhaseStep.PRECOMBAT_MAIN, playerA); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Armed", "Balduvian Bears"); setStrictChooseMode(true); @@ -118,7 +234,6 @@ public class CastSplitCardsWithCostModificationTest extends CardTestPlayerBase { } @Test - @Ignore // TODO: cost modification don't work for fused spells, only for one of the part, see https://github.com/magefree/mage/issues/6603 public void test_CostReduction_SplitFused_ReduceRight() { // {2}{W}{U} // As Council of the Absolute enters the battlefield, choose a noncreature, nonland card name. @@ -159,7 +274,6 @@ public class CastSplitCardsWithCostModificationTest extends CardTestPlayerBase { } @Test - @Ignore // TODO: cost modification don't work for fused spells, only for one of the part, see https://github.com/magefree/mage/issues/6603 public void test_CostReduction_SplitFused_ReduceLeft() { // {2}{W}{U} // As Council of the Absolute enters the battlefield, choose a noncreature, nonland card name. 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 b43b200626d..0cff76fc8d5 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 @@ -314,17 +314,7 @@ public class TestPlayer implements Player { if (group.startsWith("spell") || group.startsWith("!spell") || group.startsWith("target=null") || group.startsWith("manaInPool=")) { break; } - if (ability instanceof SpellAbility && ((SpellAbility) ability).getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED) { - if (group.contains("FuseLeft-")) { - result = handleTargetString(group.substring(group.indexOf("FuseLeft-") + 9), ability, game); - } else if (group.startsWith("FuseRight-")) { - result = handleTargetString(group.substring(group.indexOf("FuseRight-") + 10), ability, game); - } else { - result = false; - } - } else { - result = handleTargetString(group, ability, game); - } + result = handleTargetString(group, ability, game); } return result; } diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index 42c6ba27ec6..dd96e8421ea 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -124,24 +124,28 @@ public class Spell extends StackObjImpl implements Card { } public boolean activate(Game game, boolean noMana) { - setDoneActivatingManaAbilities(false); // Used for e.g. improvise - if (!spellAbilities.get(0).activate(game, noMana)) { + setDoneActivatingManaAbilities(false); // used for e.g. improvise + + if (!ability.activate(game, noMana)) { return false; } - if (spellAbilities.size() > 1) { - // if there are more abilities (fused split spell) or first ability added new abilities (splice), activate the additional abilities - boolean ignoreAbility = true; + + // spell can contains multiple abilities to activate (fused split, splice) + for (SpellAbility spellAbility : spellAbilities) { + if (ability.equals(spellAbility)) { + // activated first + continue; + } + boolean payNoMana = noMana; - for (SpellAbility spellAbility : spellAbilities) { - if (ignoreAbility) { - ignoreAbility = false; - } else { - // costs for spliced abilities were added to main spellAbility, so pay no mana for spliced abilities - payNoMana |= spellAbility.getSpellAbilityType() == SpellAbilityType.SPLICE; - if (!spellAbility.activate(game, payNoMana)) { - return false; - } - } + // costs for spliced abilities were added to main spellAbility, so pay no mana for spliced abilities + payNoMana |= spellAbility.getSpellAbilityType() == SpellAbilityType.SPLICE; + // costs for fused ability pay on first spell activate, so all parts must be without mana + // see https://github.com/magefree/mage/issues/6603 + payNoMana |= ability.getSpellAbilityType() == SpellAbilityType.SPLIT_FUSED; + + if (!spellAbility.activate(game, payNoMana)) { + return false; } } setDoneActivatingManaAbilities(true); // can be activated again maybe during the resolution of the spell (e.g. Metallic Rebuke) diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 95d6c8b320b..dbdd179cad5 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2699,11 +2699,7 @@ public abstract class PlayerImpl implements Player, Serializable { (event.isWinnable() ? "(You called " + event.getChosenName() + ")" : null), "Heads", "Tails", source, game )); - } else if (canChooseHeads) { - event.setResult(true); - } else { - event.setResult(false); - } + } else event.setResult(canChooseHeads); game.informPlayers(getLogName() + " chose to keep " + CardUtil.booleanToFlipName(event.getResult())); } if (event.isWinnable()) { @@ -2935,7 +2931,7 @@ public abstract class PlayerImpl implements Player, Serializable { */ protected boolean canPlay(ActivatedAbility ability, ManaOptions available, MageObject sourceObject, Game game) { if (!(ability instanceof ActivatedManaAbilityImpl)) { - ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability + ActivatedAbility copy = ability.copy(); // copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability if (!copy.canActivate(playerId, game).canActivate()) { return false; }