diff --git a/Mage.Sets/src/mage/cards/i/InscriptionOfAbundance.java b/Mage.Sets/src/mage/cards/i/InscriptionOfAbundance.java index 84eb6e1c1a..45677784e0 100644 --- a/Mage.Sets/src/mage/cards/i/InscriptionOfAbundance.java +++ b/Mage.Sets/src/mage/cards/i/InscriptionOfAbundance.java @@ -8,7 +8,7 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.FightTargetsEffect; import mage.abilities.effects.common.counter.AddCountersTargetEffect; -import mage.abilities.keyword.KickerAbility; +import mage.abilities.keyword.KickerWithAnyNumberModesAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -33,12 +33,11 @@ public final class InscriptionOfAbundance extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{G}"); // Kicker {2}{G} - this.addAbility(new KickerAbility(new ManaCostsImpl<>("{2}{G}"))); + this.addAbility(new KickerWithAnyNumberModesAbility(new ManaCostsImpl<>("{2}{G}"))); // Choose one. If this spell was kicked, choose any number instead. - this.getSpellAbility().getModes().setAllWhenKicked(true); - // • Put two +1/+1 counters on target creature. + this.getSpellAbility().getModes().setChooseText("choose one. If this spell was kicked, choose any number instead."); this.getSpellAbility().addEffect(new AddCountersTargetEffect(CounterType.P1P1.createInstance(2))); this.getSpellAbility().addTarget(new TargetCreaturePermanent()); diff --git a/Mage.Sets/src/mage/cards/i/InscriptionOfInsight.java b/Mage.Sets/src/mage/cards/i/InscriptionOfInsight.java index 8155354558..5db7f9c492 100644 --- a/Mage.Sets/src/mage/cards/i/InscriptionOfInsight.java +++ b/Mage.Sets/src/mage/cards/i/InscriptionOfInsight.java @@ -7,7 +7,7 @@ import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.abilities.effects.keyword.ScryEffect; -import mage.abilities.keyword.KickerAbility; +import mage.abilities.keyword.KickerWithAnyNumberModesAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -29,12 +29,11 @@ public final class InscriptionOfInsight extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{U}"); // Kicker {2}{U}{U} - this.addAbility(new KickerAbility(new ManaCostsImpl<>("{2}{U}{U}"))); + this.addAbility(new KickerWithAnyNumberModesAbility(new ManaCostsImpl<>("{2}{U}{U}"))); // Choose one. If this spell was kicked, choose any number instead. - this.getSpellAbility().getModes().setAllWhenKicked(true); - // • Return up to two target creatures to their owners' hands. + this.getSpellAbility().getModes().setChooseText("choose one. If this spell was kicked, choose any number instead."); this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); this.getSpellAbility().addTarget(new TargetCreaturePermanent(0, 2)); diff --git a/Mage.Sets/src/mage/cards/i/InscriptionOfRuin.java b/Mage.Sets/src/mage/cards/i/InscriptionOfRuin.java index b4c7c6e57d..2109ecc05c 100644 --- a/Mage.Sets/src/mage/cards/i/InscriptionOfRuin.java +++ b/Mage.Sets/src/mage/cards/i/InscriptionOfRuin.java @@ -5,7 +5,7 @@ import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.common.DestroyTargetEffect; import mage.abilities.effects.common.ReturnFromGraveyardToBattlefieldTargetEffect; import mage.abilities.effects.common.discard.DiscardTargetEffect; -import mage.abilities.keyword.KickerAbility; +import mage.abilities.keyword.KickerWithAnyNumberModesAbility; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; @@ -40,12 +40,11 @@ public final class InscriptionOfRuin extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{2}{B}"); // Kicker {2}{B}{B} - this.addAbility(new KickerAbility(new ManaCostsImpl<>("{2}{B}{B}"))); + this.addAbility(new KickerWithAnyNumberModesAbility(new ManaCostsImpl<>("{2}{B}{B}"))); // Choose one. If this spell was kicked, choose any number instead. - this.getSpellAbility().getModes().setAllWhenKicked(true); - // • Target opponent discards two cards. + this.getSpellAbility().getModes().setChooseText("choose one. If this spell was kicked, choose any number instead."); this.getSpellAbility().addEffect(new DiscardTargetEffect(2)); this.getSpellAbility().addTarget(new TargetOpponent()); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java index 92bc5d8045..437dd6a6ff 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/EntwineTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.abilities.keywords; import mage.constants.PhaseStep; @@ -7,39 +6,172 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * - * @author LevelX2 + * @author LevelX2, JayDi85 */ public class EntwineTest extends CardTestPlayerBase { + @Test + public void test_CastWithoutEntwine() { + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, "No"); // not use Entwine + setModeChoice(playerA, "1"); // target creature + addTarget(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3); + } + + @Test + public void test_CastEntwine_Normal() { + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, "Yes"); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3 + 2); + } + + @Test + public void test_CastEntwine_CostReduction() { + addCustomEffect_SpellCostModification(playerA, -4); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // -4 as cost reduction + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, "Yes"); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 1); + } + + @Test + public void test_CastEntwine_CostIncreasing() { + addCustomEffect_SpellCostModification(playerA, 5); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3 + 2 + 5); + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, "Yes"); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Mountain", true, 3 + 2 + 5); + } + + @Test + public void test_CastEntwine_FreeFromHand() { + // You may cast nonland cards from your hand without paying their mana costs. + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + + // Choose one — + //• Barbed Lightning deals 3 damage to target creature. + //• Barbed Lightning deals 3 damage to target player or planeswalker. + // Entwine {2} (Choose both if you pay the entwine cost.) + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); // only Entwine pay need + // + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, "Yes"); // cast for free + setChoice(playerA, "Yes"); // use Entwine + addTarget(playerA, "Balduvian Bears"); + addTarget(playerA, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 20 - 3); + assertPermanentCount(playerA, "Balduvian Bears", 0); + assertTappedCount("Plains", true, 2); + } + @Test public void test_ToothAndNail() { setStrictChooseMode(true); - + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); - + addCard(Zone.BATTLEFIELD, playerA, "Forest", 9); // Choose one - // Search your library for up to two creature cards, reveal them, put them into your hand, then shuffle your library; // or put up to two creature cards from your hand onto the battlefield. // Entwine {2} addCard(Zone.HAND, playerA, "Tooth and Nail"); // Sorcery {5}{G}{G} - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tooth and Nail"); setChoice(playerA, "Yes"); // Message: Pay Entwine {2} ? addTarget(playerA, "Silvercoat Lion^Pillarfield Ox"); - - setChoice(playerA, "Silvercoat Lion^Pillarfield Ox"); - + setChoice(playerA, "Silvercoat Lion^Pillarfield Ox"); setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); execute(); - assertAllCommandsUsed(); - + assertPermanentCount(playerA, "Silvercoat Lion", 1); assertPermanentCount(playerA, "Pillarfield Ox", 1); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java index 666acb1073..0728e66cf2 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerTest.java @@ -400,6 +400,7 @@ public class KickerTest extends CardTestPlayerBase { assertTappedCount("Swamp", true, 5); assertGraveyardCount(playerA, "Marsh Casualties", 1); assertPowerToughness(playerB, "Centaur Courser", 1, 1); - } + + } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java new file mode 100644 index 0000000000..05b8c998fc --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/KickerWithAnyNumberModesAbilityTest.java @@ -0,0 +1,158 @@ +package org.mage.test.cards.abilities.keywords; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author JayDi85 + */ +public class KickerWithAnyNumberModesAbilityTest extends CardTestPlayerBase { + + @Test + public void test_WithoutKicker() { + // Kicker {2}{G} + // Choose one. If this spell was kicked, choose any number instead. + // • Put two +1/+1 counters on target creature. + // • Target player gains X life, where X is the greatest power among creatures they control. + // • Target creature you control fights target creature you don't control. + addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance"); + setChoice(playerA, "No"); // no kicker + setModeChoice(playerA, "1"); + addTarget(playerA, "Balduvian Bears"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2); + assertLife(playerA, 20); + assertTappedCount("Forest", true, 2); + } + + @Test + public void test_Kicker_Normal() { + // Kicker {2}{G} + // Choose one. If this spell was kicked, choose any number instead. + // • Put two +1/+1 counters on target creature. + // • Target player gains X life, where X is the greatest power among creatures they control. + // • Target creature you control fights target creature you don't control. + addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 3); + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance"); + setChoice(playerA, "Yes"); // use kicker + setModeChoice(playerA, "2"); + setModeChoice(playerA, "1"); + addTarget(playerA, playerA); // gain x life + addTarget(playerA, "Balduvian Bears"); // get counters + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2); + assertLife(playerA, 20 + 4); + assertTappedCount("Forest", true, 2 + 3); + } + + @Test + public void test_Kicker_CostReduction() { + addCustomEffect_SpellCostModification(playerA, -4); + + // Kicker {2}{G} + // Choose one. If this spell was kicked, choose any number instead. + // • Put two +1/+1 counters on target creature. + // • Target player gains X life, where X is the greatest power among creatures they control. + // • Target creature you control fights target creature you don't control. + addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 3 - 3); // -3 by cost reduction + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance"); + setChoice(playerA, "Yes"); // use kicker + setModeChoice(playerA, "2"); + setModeChoice(playerA, "1"); + addTarget(playerA, playerA); // gain x life + addTarget(playerA, "Balduvian Bears"); // get counters + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2); + assertLife(playerA, 20 + 4); + assertTappedCount("Forest", true, 2 + 3 - 3); + } + + @Test + public void test_Kicker_CostIncreasing() { + addCustomEffect_SpellCostModification(playerA, 5); + + // Kicker {2}{G} + // Choose one. If this spell was kicked, choose any number instead. + // • Put two +1/+1 counters on target creature. + // • Target player gains X life, where X is the greatest power among creatures they control. + // • Target creature you control fights target creature you don't control. + addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2 + 3 + 5); // +5 by cost increase + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance"); + setChoice(playerA, "Yes"); // use kicker + setModeChoice(playerA, "2"); + setModeChoice(playerA, "1"); + addTarget(playerA, playerA); // gain x life + addTarget(playerA, "Balduvian Bears"); // get counters + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2); + assertLife(playerA, 20 + 4); + assertTappedCount("Forest", true, 2 + 3 + 5); + } + + @Test + public void test_Kicker_FreeFromHand() { + // You may cast nonland cards from your hand without paying their mana costs. + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + + // Kicker {2}{G} + // Choose one. If this spell was kicked, choose any number instead. + // • Put two +1/+1 counters on target creature. + // • Target player gains X life, where X is the greatest power among creatures they control. + // • Target creature you control fights target creature you don't control. + addCard(Zone.HAND, playerA, "Inscription of Abundance", 1); // {1}{G} + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); // -2 by free cast + addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Inscription of Abundance"); + setChoice(playerA, "Yes"); // use free cast + setChoice(playerA, "Yes"); // use kicker + setModeChoice(playerA, "2"); + setModeChoice(playerA, "1"); + addTarget(playerA, playerA); // gain x life + addTarget(playerA, "Balduvian Bears"); // get counters + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, "Balduvian Bears", 2 + 2, 2 + 2); + assertLife(playerA, 20 + 4); + assertTappedCount("Forest", true, 3); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java index d0b8d4e8e1..a9ddf58c33 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/alternate/CastFromHandWithoutPayingManaCostTest.java @@ -192,7 +192,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { /** * Omniscience is not allowing me to cast spells for free. I'm playing a * Commander game against the Computer, if that helps. - * + *

* Edit: It's not letting me cast fused spells for free. Others seems to be * working. */ @@ -276,16 +276,23 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { // Choose one - Barbed Lightning deals 3 damage to target creature; or Barbed Lightning deals 3 damage to target player. // Entwine {2} (Choose both if you pay the entwine cost.) - addCard(Zone.HAND, playerA, "Barbed Lightning", 1); + addCard(Zone.HAND, playerA, "Barbed Lightning", 1); // {2}{R} // Creature - 3/3 Swampwalk addCard(Zone.BATTLEFIELD, playerB, "Bog Wraith", 1); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning", "Bog Wraith"); - addTarget(playerA, playerB); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning"); + setChoice(playerA, "Yes"); // cast without cost + setChoice(playerA, "Yes"); // pay Entwine + addTarget(playerA, "Bog Wraith"); // target form mode 1 + addTarget(playerA, playerB); // target for mode 2 - setStopAt(1, PhaseStep.BEGIN_COMBAT); + showBattlefield("after", 1, PhaseStep.POSTCOMBAT_MAIN, playerA); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Barbed Lightning", 1); assertGraveyardCount(playerB, "Bog Wraith", 1); @@ -293,7 +300,7 @@ public class CastFromHandWithoutPayingManaCostTest extends CardTestPlayerBase { assertLife(playerA, 20); assertLife(playerB, 17); - assertTapped("Plains", true); // plains have to be tapped because {2} from Entwine have to be paid + assertTappedCount("Plains", true, 2); // plains have to be tapped because {2} from Entwine have to be paid } /** diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/modal/ChooseOneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/modal/ChooseOneTest.java index 5453c1cb6c..9bed3184a4 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/modal/ChooseOneTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/modal/ChooseOneTest.java @@ -1,4 +1,3 @@ - package org.mage.test.cards.modal; import mage.abilities.keyword.SwampwalkAbility; @@ -8,7 +7,6 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * * @author LevelX2 */ public class ChooseOneTest extends CardTestPlayerBase { @@ -48,8 +46,10 @@ public class ChooseOneTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Funeral Charm", "Silvercoat Lion"); setModeChoice(playerA, "2"); + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertAllCommandsUsed(); assertGraveyardCount(playerA, "Funeral Charm", 1); assertPowerToughness(playerB, "Silvercoat Lion", 4, 1); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 36567af94b..eb33a433ba 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1,9 +1,5 @@ package mage.abilities; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; import mage.MageIdentifier; import mage.MageObject; import mage.abilities.costs.*; @@ -36,6 +32,11 @@ import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import org.apache.log4j.Logger; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -227,22 +228,10 @@ public abstract class AbilityImpl implements Ability { } game.applyEffects(); - /* 20130201 - 601.2b - * If the spell is modal the player announces the mode choice (see rule 700.2). - */ - if (!getModes().choose(game, this)) { - return false; - } - MageObject sourceObject = getSourceObject(game); if (getSourceObjectZoneChangeCounter() == 0) { setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(getSourceId())); } - if (controller.isTestMode()) { - if (!controller.addTargets(this, game)) { - return false; - } - } /* 20130201 - 601.2b * If the player wishes to splice any cards onto the spell (see rule 702.45), he @@ -268,9 +257,6 @@ public abstract class AbilityImpl implements Ability { } } - if (getModes().getAdditionalCost() != null) { - getModes().getAdditionalCost().addOptionalAdditionalModeCosts(this, game); - } // 20130201 - 601.2b // If the spell has alternative or additional costs that will be paid as it's being cast such // as buyback, kicker, or convoke costs (see rules 117.8 and 117.9), the player announces his @@ -311,8 +297,29 @@ public abstract class AbilityImpl implements Ability { handlePhyrexianManaCosts(game, sourceId, controller); + /* 20130201 - 601.2b + * If the spell is modal the player announces the mode choice (see rule 700.2). + */ + // rules: + // You kick a spell as you cast it. You declare whether you're going to pay a kicker cost at the same + // time you'd choose a spell's mode, and then you actually pay it at the same time you pay the spell's mana cost. + // Kicking a spell is always optional. + if (!getModes().choose(game, this)) { + return false; + } + + // unit tests only: it allows to add targets/choices by two ways: + // 1. From cast/activate command params (process it here) + // 2. From single addTarget/setChoice, it's a preffered method for tests (process it in normal choose dialogs like human player) + if (controller.isTestMode()) { + if (!controller.addTargets(this, game)) { + return false; + } + } + for (UUID modeId : this.getModes().getSelectedModes()) { this.getModes().setActiveMode(modeId); + //20121001 - 601.2c // 601.2c The player announces their choice of an appropriate player, object, or zone for // each target the spell requires. A spell may require some targets only if an alternative or @@ -333,8 +340,10 @@ public abstract class AbilityImpl implements Ability { if (sourceObject != null && this.getAbilityType() != AbilityType.TRIGGERED) { // triggered abilities check this already in playerImpl.triggerAbility sourceObject.adjustTargets(this, game); } + if (!getTargets().isEmpty()) { Outcome outcome = getEffects().getOutcome(this); + // only activated abilities can be canceled by human user (not triggered) boolean canCancel = this instanceof ActivatedAbility && controller.isHuman(); if (!getTargets().chooseTargets(outcome, this.controllerId, this, noMana, game, canCancel)) { @@ -434,7 +443,11 @@ public abstract class AbilityImpl implements Ability { boolean alternativeCostUsed = false; if (sourceObject != null && !(sourceObject instanceof Permanent)) { + // it's important to apply alternative cost first + // example: Omniscience gives free mana as alternative, but Entwine ability adds {2} as additional Abilities abilities = CardUtil.getAbilities(sourceObject, game); + + // 1. ALTERNATIVE COSTS for (Ability ability : abilities) { // if cast for noMana no Alternative costs are allowed if (canUseAlternativeCost && !noMana && ability instanceof AlternativeSourceCosts) { @@ -447,11 +460,7 @@ public abstract class AbilityImpl implements Ability { } } } - if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) { - ((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game); - } } - // controller specific alternate spell costs if (canUseAlternativeCost && !noMana && !alternativeCostUsed) { if (this.getAbilityType() == AbilityType.SPELL @@ -469,6 +478,13 @@ public abstract class AbilityImpl implements Ability { } } } + + // 2. ADDITIONAL COST + for (Ability ability : abilities) { + if (canUseAdditionalCost && ability instanceof OptionalAdditionalSourceCosts) { + ((OptionalAdditionalSourceCosts) ability).addOptionalAdditionalCosts(this, game); + } + } } return alternativeCostUsed; @@ -806,7 +822,18 @@ public abstract class AbilityImpl implements Ability { @Override public void addCost(Cost cost) { - if (cost != null) { + if (cost == null) { + return; + } + + if (cost instanceof Costs) { + // as list of costs + Costs list = (Costs) cost; + for (Cost single : list) { + addCost(single); + } + } else { + // as single cost if (cost instanceof ManaCost) { this.addManaCost((ManaCost) cost); } else { diff --git a/Mage/src/main/java/mage/abilities/Modes.java b/Mage/src/main/java/mage/abilities/Modes.java index bc0f008111..2f1f7ab6bd 100644 --- a/Mage/src/main/java/mage/abilities/Modes.java +++ b/Mage/src/main/java/mage/abilities/Modes.java @@ -1,6 +1,5 @@ package mage.abilities; -import mage.abilities.condition.common.KickedCondition; import mage.abilities.costs.OptionalAdditionalModeSourceCosts; import mage.cards.Card; import mage.constants.Outcome; @@ -33,11 +32,9 @@ public class Modes extends LinkedHashMap { private TargetController modeChooser; private boolean eachModeMoreThanOnce; // each mode can be selected multiple times during one choice private boolean eachModeOnlyOnce; // state if each mode can be chosen only once as long as the source object exists - private OptionalAdditionalModeSourceCosts optionalAdditionalModeSourceCosts = null; // only set if costs have to be paid private Filter maxModesFilter = null; // calculates the max number of available modes private boolean isRandom = false; private String chooseText = null; - private boolean allWhenKicked = false; private boolean resetEachTurn = false; private int turnNum = 0; @@ -68,12 +65,10 @@ public class Modes extends LinkedHashMap { this.modeChooser = modes.modeChooser; this.eachModeOnlyOnce = modes.eachModeOnlyOnce; this.eachModeMoreThanOnce = modes.eachModeMoreThanOnce; - this.optionalAdditionalModeSourceCosts = modes.optionalAdditionalModeSourceCosts; this.maxModesFilter = modes.maxModesFilter; // can't change so no copy needed this.isRandom = modes.isRandom; this.chooseText = modes.chooseText; - this.allWhenKicked = modes.allWhenKicked; this.resetEachTurn = modes.resetEachTurn; this.turnNum = modes.turnNum; if (modes.getSelectedModes().isEmpty()) { @@ -228,15 +223,6 @@ public class Modes extends LinkedHashMap { this.turnNum = game.getTurnNum(); } } - if (this.allWhenKicked) { - if (KickedCondition.instance.apply(game, source)) { - this.setMinModes(0); - this.setMaxModes(3); - } else { - this.setMinModes(1); - this.setMaxModes(1); - } - } if (this.size() > 1) { this.clearSelectedModes(); if (this.isRandom) { @@ -244,15 +230,18 @@ public class Modes extends LinkedHashMap { this.addSelectedMode(modes.get(RandomUtil.nextInt(modes.size())).getId()); return true; } + // check if mode modifying abilities exist Card card = game.getCard(source.getSourceId()); if (card != null) { - for (Ability modeModifyingAbility : card.getAbilities()) { + for (Ability modeModifyingAbility : card.getAbilities(game)) { if (modeModifyingAbility instanceof OptionalAdditionalModeSourceCosts) { + // cost must check activation conditional in changeModes ((OptionalAdditionalModeSourceCosts) modeModifyingAbility).changeModes(source, game); } } } + // check if all modes can be activated automatically if (this.size() == this.getMinModes() && !isEachModeMoreThanOnce()) { Set onceSelectedModes = null; @@ -434,8 +423,6 @@ public class Modes extends LinkedHashMap { StringBuilder sb = new StringBuilder(); if (this.chooseText != null) { sb.append(chooseText); - } else if (this.allWhenKicked) { - sb.append("choose one. If this spell was kicked, choose any number instead."); } else if (this.getMaxModesFilter() != null) { sb.append("choose one or more. Each mode must target ").append(getMaxModesFilter().getMessage()); } else if (this.getMinModes() == 0 && this.getMaxModes() == 1) { @@ -499,22 +486,10 @@ public class Modes extends LinkedHashMap { this.eachModeMoreThanOnce = eachModeMoreThanOnce; } - public OptionalAdditionalModeSourceCosts getAdditionalCost() { - return optionalAdditionalModeSourceCosts; - } - - public void setAdditionalCost(OptionalAdditionalModeSourceCosts optionalAdditionalModeSourceCosts) { - this.optionalAdditionalModeSourceCosts = optionalAdditionalModeSourceCosts; - } - public void setRandom(boolean isRandom) { this.isRandom = isRandom; } - public void setAllWhenKicked(boolean allWhenKicked) { - this.allWhenKicked = allWhenKicked; - } - public boolean isResetEachTurn() { return resetEachTurn; } diff --git a/Mage/src/main/java/mage/abilities/costs/CostsImpl.java b/Mage/src/main/java/mage/abilities/costs/CostsImpl.java index f4f1c1acae..b3ea32b880 100644 --- a/Mage/src/main/java/mage/abilities/costs/CostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/CostsImpl.java @@ -1,4 +1,3 @@ - package mage.abilities.costs; import java.util.ArrayList; @@ -161,5 +160,4 @@ public class CostsImpl extends ArrayList implements Costs public Costs copy() { return new CostsImpl(this); } - } diff --git a/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalCost.java b/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalCost.java index 41e4ad4491..ae43332a02 100644 --- a/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalCost.java +++ b/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalCost.java @@ -1,10 +1,11 @@ - package mage.abilities.costs; +import mage.util.Copyable; + /** * @author LevelX2 */ -public interface OptionalAdditionalCost extends Cost { +public interface OptionalAdditionalCost extends Cost, Copyable { String getName(); @@ -29,20 +30,18 @@ public interface OptionalAdditionalCost extends Cost { * message. * * @param position - if there are multiple costs, it's the postion the cost - * is set (starting with 0) + * is set (starting with 0) * @return */ String getCastSuffixMessage(int position); /** * If the player intends to pay the cost, the cost will be activated - * */ void activate(); /** * Reset the activate and count information - * */ void reset(); diff --git a/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalModeSourceCosts.java b/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalModeSourceCosts.java index 0e6ffa9f7a..453a2fd361 100644 --- a/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalModeSourceCosts.java +++ b/Mage/src/main/java/mage/abilities/costs/OptionalAdditionalModeSourceCosts.java @@ -1,18 +1,15 @@ - package mage.abilities.costs; import mage.abilities.Ability; import mage.game.Game; /** + * Cost that can change ability's modes (example: Kicker or Entwine can make all modes selectabled). * * @author LevelX2 */ -public interface OptionalAdditionalModeSourceCosts { - - void addOptionalAdditionalModeCosts(Ability ability, Game game); +public interface OptionalAdditionalModeSourceCosts extends OptionalAdditionalSourceCosts { void changeModes(Ability ability, Game game); - String getCastMessageSuffix(); } diff --git a/Mage/src/main/java/mage/abilities/keyword/EntwineAbility.java b/Mage/src/main/java/mage/abilities/keyword/EntwineAbility.java index d2ebcbad1c..299712952f 100644 --- a/Mage/src/main/java/mage/abilities/keyword/EntwineAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/EntwineAbility.java @@ -3,14 +3,19 @@ package mage.abilities.keyword; import mage.abilities.Ability; import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; -import mage.abilities.costs.*; +import mage.abilities.costs.Cost; +import mage.abilities.costs.OptionalAdditionalCost; +import mage.abilities.costs.OptionalAdditionalCostImpl; +import mage.abilities.costs.OptionalAdditionalModeSourceCosts; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.constants.AbilityType; import mage.constants.Outcome; import mage.constants.Zone; import mage.game.Game; import mage.players.Player; -import java.util.Iterator; +import java.util.HashSet; +import java.util.Set; /** * 702.40. Entwine @@ -24,20 +29,23 @@ import java.util.Iterator; * 702.40b If the entwine cost was paid, follow the text of each of the modes in * the order written on the card when the spell resolves. * - * @author LevelX2 + * @author JayDi85 */ public class EntwineAbility extends StaticAbility implements OptionalAdditionalModeSourceCosts { private static final String keywordText = "Entwine"; + protected static final String reminderText = "You may {cost} in addition to any other costs to use all modes."; + protected OptionalAdditionalCost additionalCost; + protected Set activations = new HashSet<>(); // same logic as KickerAbility: activations per zoneChangeCounter public EntwineAbility(String manaString) { super(Zone.STACK, null); - this.additionalCost = new OptionalAdditionalCostImpl(keywordText, "Choose both if you pay the entwine cost.", new ManaCostsImpl(manaString)); + this.additionalCost = new OptionalAdditionalCostImpl(keywordText, reminderText, new ManaCostsImpl(manaString)); } public EntwineAbility(Cost cost) { - this(cost, "Choose both if you pay the entwine cost."); + this(cost, reminderText); } public EntwineAbility(Cost cost, String reminderText) { @@ -48,7 +56,10 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM public EntwineAbility(final EntwineAbility ability) { super(ability); - additionalCost = ability.additionalCost; + if (ability.additionalCost != null) { + this.additionalCost = ability.additionalCost.copy(); + } + this.activations.addAll(ability.activations); } @Override @@ -57,61 +68,25 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM } @Override - public void addCost(Cost cost) { - if (additionalCost != null) { - ((Costs) additionalCost).add(cost); - } - } - - @Override - public boolean isActivated() { - if (additionalCost != null) { - return additionalCost.isActivated(); - } - return false; - } - - public void resetCosts() { - if (additionalCost != null) { - additionalCost.reset(); - } - } - - @Override - public void changeModes(Ability ability, Game game) { + public void addOptionalAdditionalCosts(Ability ability, Game game) { if (!(ability instanceof SpellAbility)) { return; } + Player player = game.getPlayer(ability.getControllerId()); if (player == null) { return; } - this.resetCosts(); + + this.resetCosts(game, ability); if (additionalCost == null) { return; } + if (additionalCost.canPay(ability, ability.getSourceId(), ability.getControllerId(), game) && player.chooseUse(Outcome.Benefit, "Pay " + additionalCost.getText(false) + " ?", ability, game)) { - - additionalCost.activate(); - int modeCount = ability.getModes().size(); - ability.getModes().setAdditionalCost(this); - ability.getModes().setMinModes(modeCount); - ability.getModes().setMaxModes(modeCount); - } - } - - @Override - public void addOptionalAdditionalModeCosts(Ability ability, Game game) { - if (additionalCost.isActivated()) { - for (Iterator it = ((Costs) additionalCost).iterator(); it.hasNext();) { - Cost cost = (Cost) it.next(); - if (cost instanceof ManaCostsImpl) { - ability.getManaCostsToPay().add((ManaCostsImpl) cost.copy()); - } else { - ability.getCosts().add(cost.copy()); - } - } + addCostsToAbility(additionalCost, ability); + activateCost(game, ability); } } @@ -134,11 +109,52 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM } } - public String getReminderText() { - if (additionalCost != null) { - return additionalCost.getReminderText(); - } else { - return ""; + public void changeModes(Ability ability, Game game) { + if (!costWasActivated(ability, game)) { + return; } + + // activate max modes all the time + int maxModes = ability.getModes().size(); + ability.getModes().setMinModes(maxModes); + ability.getModes().setMaxModes(maxModes); + } + + private void addCostsToAbility(Cost cost, Ability ability) { + ability.addCost(cost.copy()); + } + + private void resetCosts(Game game, Ability source) { + if (additionalCost != null) { + additionalCost.reset(); + } + + String key = getActivationKey(source, game); + this.activations.remove(key); + } + + private void activateCost(Game game, Ability source) { + String key = getActivationKey(source, game); + this.activations.add(key); + } + + public boolean costWasActivated(Ability ability, Game game) { + String key = getActivationKey(ability, game); + return this.activations.contains(key); + } + + private String getActivationKey(Ability source, Game game) { + // same logic as KickerAbility, uses for source ability only + int zcc = 0; + if (source.getAbilityType() == AbilityType.TRIGGERED) { + zcc = source.getSourceObjectZoneChangeCounter(); + } + if (zcc == 0) { + zcc = game.getState().getZoneChangeCounter(source.getSourceId()); + } + if (zcc > 0 && (source.getAbilityType() == AbilityType.TRIGGERED)) { + --zcc; + } + return String.valueOf(zcc); } } diff --git a/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java b/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java index c1fa3c209c..6728e0e6af 100644 --- a/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/KickerAbility.java @@ -82,7 +82,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo public KickerAbility(final KickerAbility ability) { super(ability); for (OptionalAdditionalCost cost : ability.kickerCosts) { - this.kickerCosts.add((OptionalAdditionalCost) cost.copy()); + this.kickerCosts.add(cost.copy()); } this.keywordText = ability.keywordText; this.reminderText = ability.reminderText; @@ -113,7 +113,7 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo cost.reset(); } String key = getActivationKey(source, "", game); - for (Iterator iterator = activations.keySet().iterator(); iterator.hasNext();) { + for (Iterator iterator = activations.keySet().iterator(); iterator.hasNext(); ) { String activationKey = iterator.next(); if (activationKey.startsWith(key) && activations.get(activationKey) > 0) { @@ -192,15 +192,15 @@ public class KickerAbility extends StaticAbility implements OptionalAdditionalSo // canPay checks only single mana available, not total mana usage if (kickerCost.canPay(ability, sourceId, ability.getControllerId(), game) && player.chooseUse(/*Outcome.Benefit*/Outcome.AIDontUseIt, - "Pay " + times + kickerCost.getText(false) + " ?", ability, game)) { + "Pay " + times + kickerCost.getText(false) + " ?", ability, game)) { this.activateKicker(kickerCost, ability, game); if (kickerCost instanceof Costs) { - for (Iterator itKickerCost = ((Costs) kickerCost).iterator(); itKickerCost.hasNext();) { + for (Iterator itKickerCost = ((Costs) kickerCost).iterator(); itKickerCost.hasNext(); ) { Object kickerCostObject = itKickerCost.next(); if ((kickerCostObject instanceof Costs) || (kickerCostObject instanceof CostsImpl)) { for (@SuppressWarnings("unchecked") Iterator itDetails - = ((Costs) kickerCostObject).iterator(); itDetails.hasNext();) { + = ((Costs) kickerCostObject).iterator(); itDetails.hasNext(); ) { addKickerCostsToAbility(itDetails.next(), ability, game); } } else { diff --git a/Mage/src/main/java/mage/abilities/keyword/KickerWithAnyNumberModesAbility.java b/Mage/src/main/java/mage/abilities/keyword/KickerWithAnyNumberModesAbility.java new file mode 100644 index 0000000000..f728ca6615 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/KickerWithAnyNumberModesAbility.java @@ -0,0 +1,40 @@ +package mage.abilities.keyword; + +import mage.abilities.Ability; +import mage.abilities.costs.Cost; +import mage.abilities.costs.OptionalAdditionalModeSourceCosts; +import mage.game.Game; + +/** + * Same as KickerAbility, but can enable any number modes in spell ability + * + * @author JayDi85 + */ +public class KickerWithAnyNumberModesAbility extends KickerAbility implements OptionalAdditionalModeSourceCosts { + + public KickerWithAnyNumberModesAbility(Cost cost) { + super(cost); + } + + public KickerWithAnyNumberModesAbility(final KickerWithAnyNumberModesAbility ability) { + super(ability); + } + + @Override + public void changeModes(Ability ability, Game game) { + if (!isKicked(game, ability, "")) { + return; + } + + // activate any number modes + int maxModes = ability.getModes().size(); + ability.getModes().setMinModes(0); + ability.getModes().setMaxModes(maxModes); + } + + @Override + public KickerWithAnyNumberModesAbility copy() { + return new KickerWithAnyNumberModesAbility(this); + } + +} diff --git a/Mage/src/main/java/mage/abilities/keyword/MultikickerAbility.java b/Mage/src/main/java/mage/abilities/keyword/MultikickerAbility.java index 956b2f6120..0406264f2c 100644 --- a/Mage/src/main/java/mage/abilities/keyword/MultikickerAbility.java +++ b/Mage/src/main/java/mage/abilities/keyword/MultikickerAbility.java @@ -2,6 +2,7 @@ package mage.abilities.keyword; +import mage.abilities.condition.common.KickedCondition; import mage.abilities.costs.Cost; import mage.abilities.costs.OptionalAdditionalCost;