diff --git a/Mage.Sets/src/mage/cards/m/ManaVault.java b/Mage.Sets/src/mage/cards/m/ManaVault.java index 007f4fbbf10..a895a164276 100644 --- a/Mage.Sets/src/mage/cards/m/ManaVault.java +++ b/Mage.Sets/src/mage/cards/m/ManaVault.java @@ -1,6 +1,9 @@ package mage.cards.m; import mage.Mana; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.ConditionTrueHint; +import mage.abilities.hint.ValueConditionHint; import mage.abilities.triggers.BeginningOfDrawTriggeredAbility; import mage.abilities.triggers.BeginningOfUpkeepTriggeredAbility; import mage.abilities.common.SimpleStaticAbility; @@ -33,13 +36,14 @@ public final class ManaVault extends CardImpl { // At the beginning of your upkeep, you may pay {4}. If you do, untap Mana Vault. this.addAbility(new BeginningOfUpkeepTriggeredAbility( new DoIfCostPaid(new UntapSourceEffect(), new GenericManaCost(4), "Pay {4} to untap {this}?") + .withChooseHint(new ConditionHint(SourceTappedCondition.TAPPED)) )); // At the beginning of your draw step, if Mana Vault is tapped, it deals 1 damage to you. this.addAbility(new BeginningOfDrawTriggeredAbility(new DamageControllerEffect(1, "it"), false).withInterveningIf(SourceTappedCondition.TAPPED)); - // {tap}: Add {C}{C}{C}. + // {T}: Add {C}{C}{C}. this.addAbility(new SimpleManaAbility(Zone.BATTLEFIELD, Mana.ColorlessMana(3), new TapSourceCost())); } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/conditional/DoIfCostPaidTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/conditional/DoIfCostPaidTest.java index 0705a4722a2..b24e05559eb 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/conditional/DoIfCostPaidTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/conditional/DoIfCostPaidTest.java @@ -6,13 +6,12 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBase; /** - * - * @author Quercitron + * @author Quercitron, JayDi85 */ public class DoIfCostPaidTest extends CardTestPlayerBase { @Test - public void testPayIsNotOptional() { + public void test_NonOptional() { addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // Shock deals 2 damage to any target. addCard(Zone.HAND, playerA, "Shock", 1); @@ -22,6 +21,8 @@ public class DoIfCostPaidTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerB, "Awaken the Sky Tyrant"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Shock", playerB); + + setStrictChooseMode(true); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); @@ -29,4 +30,72 @@ public class DoIfCostPaidTest extends CardTestPlayerBase { assertPermanentCount(playerB, "Dragon Token", 1); } + @Test + public void test_Optional_ManaVault_1() { + // Mana Vault doesn't untap during your untap step. + // At the beginning of your upkeep, you may pay {4}. If you do, untap Mana Vault. + // At the beginning of your draw step, if Mana Vault is tapped, it deals 1 damage to you. + // {T}: Add {C}{C}{C}. + addCard(Zone.BATTLEFIELD, playerA, "Mana Vault", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + + // turn 1 - untapped and ask about untap + setChoice(playerA, false); // do not pay + checkPermanentTapped("must be untapped on start", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mana Vault", false, 1); + checkLife("no damage on untapped", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 20); + + // turn 2 - tap + activateManaAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {C}"); + checkPermanentTapped("must be tapped after usage", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Mana Vault", true, 1); + + // turn 3 - tapped and ask about untap (do not pay) + setChoice(playerA, false); + checkPermanentTapped("must be tapped after dialog", 3, PhaseStep.PRECOMBAT_MAIN, playerA, "Mana Vault", true, 1); + checkLife("must do damage on tapped", 3, PhaseStep.PRECOMBAT_MAIN, playerA, 20 - 1); + + // turn 4 - nothing + + // turn 5 - tapped and ask about untap (do pay) + setChoice(playerA, true); + checkPermanentTapped("must be untapped after dialog", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Mana Vault", false, 1); + checkLife("no damage on untapped", 5, PhaseStep.PRECOMBAT_MAIN, playerA, 20 - 1); // -1 from old damage + + setStrictChooseMode(true); + setStopAt(5, PhaseStep.END_TURN); + execute(); + } + + @Test + public void test_Optional_ManaVault_2() { + // Make sure it allow to pay untap cost anyway, so some combos can be used, see https://github.com/magefree/mage/issues/2656 + // Example: + // When Mana Vault is untapped you can respond to the trigger pay four to untap is to tap it and then pay 4 + // to untap it. That would not be possible if the trigger is skipped. + // That's legal according to the rules and would net mana if Mana Reflection was in play, would allow + // self milling with Mesmeric Orb in play, and I'm sure many other interactions that change the game state. + + // Mana Vault doesn't untap during your untap step. + // At the beginning of your upkeep, you may pay {4}. If you do, untap Mana Vault. + // At the beginning of your draw step, if Mana Vault is tapped, it deals 1 damage to you. + // {T}: Add {C}{C}{C}. + addCard(Zone.BATTLEFIELD, playerA, "Mana Vault", 1); + // + // If you tap a permanent for mana, it produces twice as much of that mana instead. + addCard(Zone.BATTLEFIELD, playerA, "Mana Reflection", 1); + // + // Whenever a permanent becomes untapped, that permanent's controller puts the top card of + // their library into their graveyard. + addCard(Zone.BATTLEFIELD, playerA, "Mesmeric Orb", 1); + + setChoice(playerA, true); // pay by itself + checkPermanentTapped("must be untapped", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mana Vault", false, 1); + checkLife("no damage on untapped", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 20); + + checkGraveyardCount("must mill", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mountain", 1); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + } + } diff --git a/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java b/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java index c8c408787d3..e577f5d4ea0 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java +++ b/Mage/src/main/java/mage/abilities/effects/common/DoIfCostPaid.java @@ -8,6 +8,7 @@ import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; import mage.abilities.effects.OneShotEffect; +import mage.abilities.hint.Hint; import mage.constants.Outcome; import mage.game.Game; import mage.players.Player; @@ -20,6 +21,7 @@ public class DoIfCostPaid extends OneShotEffect { protected final Cost cost; private final String chooseUseText; private final boolean optional; + private Hint chooseHint = null; public DoIfCostPaid(Effect effectOnPaid, Cost cost) { this(effectOnPaid, cost, null); @@ -63,6 +65,7 @@ public class DoIfCostPaid extends OneShotEffect { this.cost = effect.cost.copy(); this.chooseUseText = effect.chooseUseText; this.optional = effect.optional; + this.chooseHint = effect.chooseHint; } public DoIfCostPaid addEffect(Effect effect) { @@ -75,6 +78,19 @@ public class DoIfCostPaid extends OneShotEffect { return this; } + /** + * Allow to add additional info in pay dialog, so user can split it in diff use cases to remember by right click + * Example: ignore untap payment for already untapped permanent like Mana Vault + */ + public DoIfCostPaid withChooseHint(Hint chooseHint) { + if (!this.optional) { + throw new IllegalArgumentException("Wrong code usage: chooseHint can be used for optional dialogs only"); + } + + this.chooseHint = chooseHint; + return this; + } + @Override public boolean apply(Game game, Ability source) { Player player = getPayingPlayer(game, source); @@ -82,11 +98,16 @@ public class DoIfCostPaid extends OneShotEffect { if (player == null || mageObject == null) { return false; } - String message = CardUtil.replaceSourceName(makeChooseText(source), mageObject.getName()); + + // nothing to pay (do not support mana cost - it's true all the time) + if (!this.cost.canPay(source, source, player.getId(), game)) { + return false; + } + + String message = CardUtil.replaceSourceName(makeChooseText(game, source), mageObject.getName()); Outcome payOutcome = executingEffects.getOutcome(source, this.outcome); - boolean canPay = cost.canPay(source, source, player.getId(), game); boolean didPay = false; - if (canPay && (!optional || player.chooseUse(payOutcome, message, source, game))) { + if (!optional || player.chooseUse(payOutcome, message, source, game)) { cost.clearPaid(); int bookmark = game.bookmarkState(); if (cost.pay(source, game, source, player.getId(), false)) { @@ -121,18 +142,26 @@ public class DoIfCostPaid extends OneShotEffect { } } - private String makeChooseText(Ability source) { - if (chooseUseText != null && !chooseUseText.isEmpty()) { - return chooseUseText; + private String makeChooseText(Game game, Ability source) { + // static + String res = chooseUseText; + + // dynamic + if (res == null || res.isEmpty()) { + String effectText = executingEffects.getText(source.getModes().getMode()); + if (!effectText.isEmpty() && effectText.charAt(effectText.length() - 1) == '.') { + effectText = effectText.substring(0, effectText.length() - 1); + } + res = CardUtil.addCostVerb(cost.getText()) + (effectText.isEmpty() ? "" : " and " + effectText) + "?"; + res = Character.toUpperCase(res.charAt(0)) + res.substring(1); } - String message; - String effectText = executingEffects.getText(source.getModes().getMode()); - if (!effectText.isEmpty() && effectText.charAt(effectText.length() - 1) == '.') { - effectText = effectText.substring(0, effectText.length() - 1); + + // additional hint, so user can remember it + if (this.chooseHint != null) { + res += String.format(" (%s)", this.chooseHint.getText(game, source)); } - message = CardUtil.addCostVerb(cost.getText()) + (effectText.isEmpty() ? "" : " and " + effectText) + "?"; - message = Character.toUpperCase(message.charAt(0)) + message.substring(1); - return message; + + return res; } protected Player getPayingPlayer(Game game, Ability source) {