diff --git a/Mage.Sets/src/mage/sets/onslaught/FutureSight.java b/Mage.Sets/src/mage/sets/onslaught/FutureSight.java index 0c8dff1f1c6..929246b111d 100644 --- a/Mage.Sets/src/mage/sets/onslaught/FutureSight.java +++ b/Mage.Sets/src/mage/sets/onslaught/FutureSight.java @@ -35,7 +35,6 @@ import mage.cards.CardImpl; import mage.constants.CardType; import mage.constants.Rarity; import mage.constants.Zone; -import mage.filter.FilterCard; /** * @@ -47,11 +46,10 @@ public class FutureSight extends CardImpl { super(ownerId, 84, "Future Sight", Rarity.RARE, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}{U}{U}"); this.expansionSetCode = "ONS"; - // Play with the top card of your library revealed. this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayWithTheTopCardRevealedEffect())); // You may play the top card of your library. - this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayTheTopCardEffect(new FilterCard()))); + this.addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new PlayTheTopCardEffect())); } public FutureSight(final FutureSight card) { diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpliceOnArcaneTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpliceOnArcaneTest.java index 0cd925879a7..f67350c1d85 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpliceOnArcaneTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/abilities/keywords/SpliceOnArcaneTest.java @@ -115,7 +115,7 @@ public class SpliceOnArcaneTest extends CardTestPlayerBase { addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); // You may exile a green card with converted mana cost X from your hand rather than pay Nourishing Shoal's mana cost. // You gain X life. - addCard(Zone.HAND, playerA, "Nourishing Shoal", 1); + addCard(Zone.HAND, playerA, "Nourishing Shoal", 1); // {X}{G}{G} addCard(Zone.HAND, playerA, "Giant Growth", 1); // You may put a creature card from your hand onto the battlefield. That creature gains haste. Sacrifice that creature at the beginning of the next end step. // Splice onto Arcane {2}{R}{R} (As you cast an Arcane spell, you may reveal this card from your hand and pay its splice cost. If you do, add this card's effects to that spell.) diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/OmniscienceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/OmniscienceTest.java index ad4f7febe71..fe2d8ea9fb3 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/OmniscienceTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/OmniscienceTest.java @@ -192,4 +192,104 @@ public class OmniscienceTest extends CardTestPlayerBase { assertGraveyardCount(playerB, "Pillarfield Ox", 1); } + /** + * If another effect (e.g. Future Sight) allows you to cast nonland cards + * from zones other than your hand, Xmage incorrectly lets you cast those + * cards without paying their mana costs. Omniscience only lets you cast + * spells from your hand without paying their mana costs. + */ + @Test + public void testCastingWithFutureSight() { + // You may cast nonland cards from your hand without paying their mana costs. + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + // Play with the top card of your library revealed. + // You may play the top card of your library. + addCard(Zone.BATTLEFIELD, playerA, "Future Sight", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 1); + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Silvercoat Lion"); + setChoice(playerA, "Yes"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertLife(playerA, 20); + assertLife(playerB, 20); + + assertPermanentCount(playerA, "Silvercoat Lion", 1); + assertTapped("Plains", true); // plains have to be tapped because {2} have to be paid + } + + /** + * If a spell has an additional cost (optional or mandatory, e.g. Entwine), + * Omniscience incorrectly allows you cast the spell as if that cost had + * been paid without paying that spell's mana cost. 117.9d If an alternative + * cost is being paid to cast a spell, any additional costs, cost increases, + * and cost reductions that affect that spell are applied to that + * alternative cost. (See rule 601.2f.) + */ + @Test + public void testCastingWithCyclonicRiftWithOverload() { + // You may cast nonland cards from your hand without paying their mana costs. + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + + // 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); + + // Creature - 3/3 Swampwalk + addCard(Zone.BATTLEFIELD, playerB, "Bog Wraith", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Barbed Lightning", "Bog Wraith"); + addTarget(playerA, playerB); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Barbed Lightning", 1); + assertGraveyardCount(playerB, "Bog Wraith", 1); + + assertLife(playerA, 20); + assertLife(playerB, 17); + + assertTapped("Plains", true); // plains have to be tapped because {2} from Entwine have to be paid + } + + /** + * If a spell has an unpayable cost (e.g. Ancestral Vision, which has no + * mana cost), Omniscience should allow you to cast that spell without + * paying its mana cost. In the case of Ancestral Vision, for example, Xmage + * only gives you the option to suspend Ancestral Vision. 117.6a If an + * unpayable cost is increased by an effect or an additional cost is + * imposed, the cost is still unpayable. If an alternative cost is applied + * to an unpayable cost, including an effect that allows a player to cast a + * spell without paying its mana cost, the alternative cost may be paid. + */ + @Test + public void testCastingUnpayableCost() { + // You may cast nonland cards from your hand without paying their mana costs. + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + + // Suspend 4-{U} + // Target player draws three cards. + addCard(Zone.HAND, playerA, "Ancestral Vision", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ancestral Vision", playerA); + addTarget(playerA, playerB); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertGraveyardCount(playerA, "Ancestral Vision", 1); + + assertHandCount(playerA, 3); + assertLife(playerA, 20); + assertLife(playerB, 20); + + } + } diff --git a/Mage/src/mage/abilities/AbilityImpl.java b/Mage/src/mage/abilities/AbilityImpl.java index d7b581515ba..659d4987fb1 100644 --- a/Mage/src/mage/abilities/AbilityImpl.java +++ b/Mage/src/mage/abilities/AbilityImpl.java @@ -471,7 +471,10 @@ public abstract class AbilityImpl implements Ability { } // controller specific alternate spell costs if (!noMana && !alternativeCostisUsed) { - if (this.getAbilityType().equals(AbilityType.SPELL)) { + if (this.getAbilityType().equals(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 + && !((SpellAbility) this).getSpellAbilityType().equals(SpellAbilityType.BASE_ALTERNATE)) { for (AlternativeSourceCosts alternativeSourceCosts : controller.getAlternativeSourceCosts()) { if (alternativeSourceCosts.isAvailable(this, game)) { if (alternativeSourceCosts.askToActivateAlternativeCosts(this, game)) { diff --git a/Mage/src/mage/abilities/SpellAbility.java b/Mage/src/mage/abilities/SpellAbility.java index 1dedf4b81e7..5106b8dc61c 100644 --- a/Mage/src/mage/abilities/SpellAbility.java +++ b/Mage/src/mage/abilities/SpellAbility.java @@ -99,10 +99,6 @@ public class SpellAbility extends ActivatedAbilityImpl { && !controllerId.equals(playerId)) { return false; } - // Check if spell has no costs (not {0} mana costs), than it's not castable. E.g. for spells like Living End, that only can be cast by Suspend Ability. - if (this.getManaCosts().isEmpty() && this.getCosts().isEmpty()) { - return false; - } // Check if rule modifying events prevent to cast the spell in check playable mode if (this.isCheckPlayableMode()) { if (game.getContinuousEffects().preventedByRuleModification( diff --git a/Mage/src/mage/abilities/costs/AlternativeCostSourceAbility.java b/Mage/src/mage/abilities/costs/AlternativeCostSourceAbility.java index 5fb7fd20f90..be4893b1b45 100644 --- a/Mage/src/mage/abilities/costs/AlternativeCostSourceAbility.java +++ b/Mage/src/mage/abilities/costs/AlternativeCostSourceAbility.java @@ -29,6 +29,7 @@ package mage.abilities.costs; import java.util.Iterator; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.StaticAbility; import mage.abilities.condition.Condition; import mage.abilities.costs.mana.ManaCost; @@ -39,6 +40,7 @@ import mage.constants.Zone; import mage.filter.FilterCard; import mage.game.Game; import mage.players.Player; +import mage.util.CardUtil; /** * @@ -145,28 +147,39 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter } Player player = game.getPlayer(ability.getControllerId()); if (player != null) { - Costs alternativeCosts; + Costs alternativeCostsToCheck; if (dynamicCost != null) { - alternativeCosts = new CostsImpl<>(); - alternativeCosts.add(convertToAlternativeCost(dynamicCost.getCost(ability, game))); + alternativeCostsToCheck = new CostsImpl<>(); + alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(ability, game))); } else { - alternativeCosts = this.alternateCosts; + alternativeCostsToCheck = this.alternateCosts; } String costChoiceText; if (dynamicCost != null) { costChoiceText = dynamicCost.getText(ability, game); } else { - costChoiceText = alternativeCosts.isEmpty() ? "Cast without paying its mana cost?" : "Pay alternative costs? (" + alternativeCosts.getText() + ")"; + costChoiceText = alternativeCostsToCheck.isEmpty() ? "Cast without paying its mana cost?" : "Pay alternative costs? (" + alternativeCostsToCheck.getText() + ")"; } - if (alternativeCosts.canPay(ability, ability.getSourceId(), ability.getControllerId(), game) + if (alternativeCostsToCheck.canPay(ability, ability.getSourceId(), ability.getControllerId(), game) && player.chooseUse(Outcome.Benefit, costChoiceText, this, game)) { - ability.getManaCostsToPay().clear(); + if (ability instanceof SpellAbility) { + for (Iterator iterator = ability.getManaCostsToPay().iterator(); iterator.hasNext();) { + ManaCost manaCost = iterator.next(); + if (manaCost instanceof VariableCost) { + iterator.remove(); + } + } + CardUtil.reduceCost((SpellAbility) ability, ability.getManaCosts()); + + } else { + ability.getManaCostsToPay().clear(); + } if (!onlyMana) { ability.getCosts().clear(); } - for (Cost cost : alternativeCosts) { + for (Cost cost : alternativeCostsToCheck) { AlternativeCost2 alternateCost = (AlternativeCost2) cost; alternateCost.activate(); for (Iterator it = ((Costs) alternateCost).iterator(); it.hasNext();) { @@ -190,14 +203,14 @@ public class AlternativeCostSourceAbility extends StaticAbility implements Alter @Override public boolean isActivated(Ability source, Game game) { - Costs alternativeCosts; + Costs alternativeCostsToCheck; if (dynamicCost != null) { - alternativeCosts = new CostsImpl<>(); - alternativeCosts.add(convertToAlternativeCost(dynamicCost.getCost(source, game))); + alternativeCostsToCheck = new CostsImpl<>(); + alternativeCostsToCheck.add(convertToAlternativeCost(dynamicCost.getCost(source, game))); } else { - alternativeCosts = this.alternateCosts; + alternativeCostsToCheck = this.alternateCosts; } - for (AlternativeCost2 cost : alternativeCosts) { + for (AlternativeCost2 cost : alternativeCostsToCheck) { if (cost.isActivated(game)) { return true; } diff --git a/Mage/src/mage/abilities/keyword/EntwineAbility.java b/Mage/src/mage/abilities/keyword/EntwineAbility.java index c81d1cbd955..0604445c2a4 100644 --- a/Mage/src/mage/abilities/keyword/EntwineAbility.java +++ b/Mage/src/mage/abilities/keyword/EntwineAbility.java @@ -111,7 +111,7 @@ public class EntwineAbility extends StaticAbility implements OptionalAdditionalM if (player != null) { this.resetCosts(); if (additionalCost != null) { - if (player.chooseUse(Outcome.Benefit, new StringBuilder("Pay ").append(additionalCost.getText(false)).append(" ?").toString(), ability, game)) { + if (player.chooseUse(Outcome.Benefit, "Pay " + additionalCost.getText(false) + " ?", ability, game)) { additionalCost.activate(); for (Iterator it = ((Costs) additionalCost).iterator(); it.hasNext();) { Cost cost = (Cost) it.next(); diff --git a/Mage/src/mage/players/PlayerImpl.java b/Mage/src/mage/players/PlayerImpl.java index 8949f270970..1719603aa72 100644 --- a/Mage/src/mage/players/PlayerImpl.java +++ b/Mage/src/mage/players/PlayerImpl.java @@ -2317,15 +2317,23 @@ public abstract class PlayerImpl implements Player, Serializable { } } } - - ManaOptions abilityOptions = copy.getManaCostsToPay().getOptions(); - if (abilityOptions.size() == 0) { - return true; - } else { - for (Mana mana : abilityOptions) { - for (Mana avail : available) { - if (mana.enough(avail)) { - return true; + boolean canBeCastRegularly = true; + if (copy instanceof SpellAbility && copy.getManaCosts().isEmpty() && copy.getCosts().isEmpty()) { + // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost... + // 117.6a (...) If an alternative cost is applied to an unpayable cost, + // including an effect that allows a player to cast a spell without paying its mana cost, the alternative cost may be paid. + canBeCastRegularly = false; + } + if (canBeCastRegularly) { + ManaOptions abilityOptions = copy.getManaCostsToPay().getOptions(); + if (abilityOptions.size() == 0) { + return true; + } else { + for (Mana mana : abilityOptions) { + for (Mana avail : available) { + if (mana.enough(avail)) { + return true; + } } } }