diff --git a/Mage.Client/src/main/java/mage/client/game/GamePanel.java b/Mage.Client/src/main/java/mage/client/game/GamePanel.java index 1e23924216..2cb9a38765 100644 --- a/Mage.Client/src/main/java/mage/client/game/GamePanel.java +++ b/Mage.Client/src/main/java/mage/client/game/GamePanel.java @@ -2187,7 +2187,7 @@ public final class GamePanel extends javax.swing.JPanel { public void installComponents() { jLayeredPane.setOpaque(false); - jLayeredPane.add(abilityPicker); + jLayeredPane.add(abilityPicker, JLayeredPane.MODAL_LAYER); jLayeredPane.add(DialogManager.getManager(gameId), JLayeredPane.MODAL_LAYER, 0); abilityPicker.setVisible(false); } diff --git a/Mage.Sets/src/mage/cards/a/AsForetold.java b/Mage.Sets/src/mage/cards/a/AsForetold.java new file mode 100644 index 0000000000..62b523ddd0 --- /dev/null +++ b/Mage.Sets/src/mage/cards/a/AsForetold.java @@ -0,0 +1,228 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ +package mage.cards.a; + +import java.util.UUID; + +import mage.MageObject; +import mage.abilities.Ability; +import mage.abilities.common.BeginningOfUpkeepTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.Condition; +import mage.abilities.costs.AlternativeCostSourceAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.dynamicvalue.common.StaticValue; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.watchers.Watcher; + +/** + * + * @author stravant + * + * Note, this card is pretty hacky in its implementation due to the fact that the alternative cost system doesn't + * really support "once each turn" alternative costs in an obvious way, but it should work in all scenarios as far + * as I can see. + */ +public class AsForetold extends CardImpl { + public AsForetold(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{2}{U}"); + + + // At the beginning of your upkeep, put a time counter on As Foretold. + addAbility( + new BeginningOfUpkeepTriggeredAbility( + new AddCountersSourceEffect(CounterType.TIME.createInstance(), new StaticValue(1), true), + TargetController.YOU, + /* optional = */false)); + + // Once each turn, you may pay {0} rather than pay the mana cost for a spell you cast with converted mana cost X or less, where X is the number of time counters on As Foretold. + addAbility(new SimpleStaticAbility(Zone.BATTLEFIELD, new AsForetoldAddAltCostEffect()), new AsForetoldAltCostUsedWatcher()); + } + + public AsForetold(final AsForetold card) { + super(card); + } + + @Override + public AsForetold copy() { + return new AsForetold(this); + } +} + +/** + * Used to determine what cast objects to apply the alternative cost to + */ +class SpellWithManaCostLessThanOrEqualToCondition implements Condition { + private int counters; + + public SpellWithManaCostLessThanOrEqualToCondition(int counters) { + this.counters = counters; + } + + @Override + public boolean apply(Game game, Ability source) { + MageObject object = game.getObject(source.getSourceId()); + return object != null && !object.isLand() && object.getConvertedManaCost() <= counters; + } +} + + +/** + * Special AlternativeCostSourceAbility implementation. We wrap the call to askToActivateAlternativeCosts in order to + * tell when the alternative cost is used, and mark it as having been used this turn in the watcher + */ +class AsForetoldAlternativeCost extends AlternativeCostSourceAbility { + private UUID sourceAsForetold; + + AsForetoldAlternativeCost(UUID sourceAsForetold, int timeCounters) { + super(new ManaCostsImpl("{0}"), new SpellWithManaCostLessThanOrEqualToCondition(timeCounters)); + this.sourceAsForetold = sourceAsForetold; + } + + AsForetoldAlternativeCost(final AsForetoldAlternativeCost ability) { + super(ability); + this.sourceAsForetold = ability.sourceAsForetold; + } + + @Override + public AsForetoldAlternativeCost copy() { + return new AsForetoldAlternativeCost(this); + } + + @Override + public boolean askToActivateAlternativeCosts(Ability ability, Game game) { + boolean activated = super.askToActivateAlternativeCosts(ability, game); + if (activated) { + // Get the watcher + AsForetoldAltCostUsedWatcher asForetoldAltCostUsedWatcher = + (AsForetoldAltCostUsedWatcher)game.getState().getWatchers() + .get("asForetoldAltCostUsedWatcher", sourceAsForetold); + + // Mark as used + asForetoldAltCostUsedWatcher.markUsedThisTurn(); + } + return activated; + } +} + +/** + * The continuous effect that adds the option to pay the alternative cost if we haven't used it yet this turn + */ +class AsForetoldAddAltCostEffect extends ContinuousEffectImpl { + public AsForetoldAddAltCostEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "Once each turn, you may pay {0} rather than pay the mana cost for a spelly ou cast with converted mana cost X or less, where X is the number of time counters on {this}."; + } + + public AsForetoldAddAltCostEffect(final AsForetoldAddAltCostEffect effect) { + super(effect); + } + + @Override + public AsForetoldAddAltCostEffect copy() { + return new AsForetoldAddAltCostEffect(this); + } + + @Override + public boolean apply(Layer layer, SubLayer sublayer, Ability source, Game game) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller != null) { + Permanent sourcePermanent = game.getPermanent(source.getSourceId()); + if (sourcePermanent != null) { + // Get the watcher + AsForetoldAltCostUsedWatcher asForetoldAltCostUsedWatcher = + (AsForetoldAltCostUsedWatcher)game.getState().getWatchers() + .get("asForetoldAltCostUsedWatcher", sourcePermanent.getId()); + + // If we haven't used it yet this turn, give the option of using the zero alternative cost + if (!asForetoldAltCostUsedWatcher.hasBeenUsedThisTurn()) { + int timeCounters = sourcePermanent.getCounters(game).getCount("time"); + controller.getAlternativeSourceCosts().add(new AsForetoldAlternativeCost(sourcePermanent.getId(), timeCounters)); + } + + // Return true even if we didn't add the alt cost. We still applied the effect + return true; + } + } + return false; + } + + @Override + public boolean apply(Game game, Ability source) { + return false; + } + + @Override + public boolean hasLayer(Layer layer) { + return layer == Layer.RulesEffects; + } +} + +/** + * Watcher used as extra storage to record whether a given As Foretold has been used this turn. + * Technically speaking this watcher doesn't *watch* any GameEvents, but it does "watch" the + * alternative cost being used. That just isn't possible to watch through a game event. It's still + * helpfull to co-op the Watcher system for this since it automatically handles ZoneChangeCounter + * stuff and resetting the condition at the end of the turn. + */ +class AsForetoldAltCostUsedWatcher extends Watcher { + public AsForetoldAltCostUsedWatcher() { + super("asForetoldAltCostUsedWatcher", WatcherScope.CARD); + } + + public AsForetoldAltCostUsedWatcher(final AsForetoldAltCostUsedWatcher watcher) { + super(watcher); + } + + @Override + public void watch(GameEvent event, Game game) { + // Nothing to do, we explicitly mark used in the alternative cost + } + + public boolean hasBeenUsedThisTurn() { + return conditionMet(); + } + + public void markUsedThisTurn() { + condition = true; + } + + @Override + public AsForetoldAltCostUsedWatcher copy() { + return new AsForetoldAltCostUsedWatcher(this); + } +} \ No newline at end of file diff --git a/Mage.Sets/src/mage/cards/g/GreelMindRaker.java b/Mage.Sets/src/mage/cards/g/GreelMindRaker.java index a27293dcbb..7a5ac37e67 100644 --- a/Mage.Sets/src/mage/cards/g/GreelMindRaker.java +++ b/Mage.Sets/src/mage/cards/g/GreelMindRaker.java @@ -42,6 +42,9 @@ import mage.constants.Zone; import mage.target.TargetPlayer; import java.util.UUID; +import mage.abilities.costs.common.DiscardTargetCost; +import mage.filter.FilterCard; +import mage.target.common.TargetCardInHand; /** * @@ -61,6 +64,7 @@ public class GreelMindRaker extends CardImpl { // {X}{B}, {tap}, Discard two cards: Target player discards X cards at random. Ability ability = new SimpleActivatedAbility(Zone.BATTLEFIELD, new DiscardTargetEffect(new ManacostVariableValue(), true), new ManaCostsImpl("{X}{B}")); ability.addCost(new TapSourceCost()); + ability.addCost(new DiscardTargetCost(new TargetCardInHand(2, new FilterCard()))); ability.addTarget(new TargetPlayer()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/sets/Amonkhet.java b/Mage.Sets/src/mage/sets/Amonkhet.java index 09884a5d52..46986e5f57 100644 --- a/Mage.Sets/src/mage/sets/Amonkhet.java +++ b/Mage.Sets/src/mage/sets/Amonkhet.java @@ -72,6 +72,7 @@ public class Amonkhet extends ExpansionSet { cards.add(new SetCardInfo("Angler Drake", 41, Rarity.UNCOMMON, mage.cards.a.AnglerDrake.class)); cards.add(new SetCardInfo("Anointer Priest", 3, Rarity.COMMON, mage.cards.a.AnointerPriest.class)); cards.add(new SetCardInfo("Archfiend of Ifnir", 78, Rarity.RARE, mage.cards.a.ArchfiendOfIfnir.class)); + cards.add(new SetCardInfo("As Foretold", 42, Rarity.MYTHIC, mage.cards.a.AsForetold.class)); cards.add(new SetCardInfo("Aven Mindcensor", 5, Rarity.RARE, mage.cards.a.AvenMindcensor.class)); cards.add(new SetCardInfo("Bontu's Monument", 225, Rarity.UNCOMMON, mage.cards.b.BontusMonument.class)); cards.add(new SetCardInfo("Canyon Slough", 239, Rarity.RARE, mage.cards.c.CanyonSlough.class)); diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 1f3365e3c6..ca4f014e73 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -286,6 +286,17 @@ public abstract class AbilityImpl implements Ability { } } + // 117.6. Some mana costs contain no mana symbols. This represents an unpayable cost. An ability can + // also have an unpayable cost if its cost is based on the mana cost of an object with no mana cost. + // Attempting to cast a spell or activate an ability that has an unpayable cost is a legal action. + // However, attempting to pay an unpayable cost is an illegal action. + // + // We apply this now, *AFTER* the user has made the choice to pay an alternative cost for the + // spell. You can also still cast a spell with an unplayable cost by... not paying it's mana cost. + if (getAbilityType() == AbilityType.SPELL && getManaCostsToPay().isEmpty() && !noMana) { + return false; + } + // 20121001 - 601.2b // If the spell has a variable cost that will be paid as it's being cast (such as an {X} in // its mana cost; see rule 107.3), the player announces the value of that variable. diff --git a/Mage/src/main/java/mage/abilities/effects/EffectImpl.java b/Mage/src/main/java/mage/abilities/effects/EffectImpl.java index 89db5169f8..c0117cff6b 100644 --- a/Mage/src/main/java/mage/abilities/effects/EffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/EffectImpl.java @@ -61,8 +61,8 @@ public abstract class EffectImpl implements Effect { public EffectImpl(final EffectImpl effect) { this.id = effect.id; this.outcome = effect.outcome; - this.effectType = effect.effectType; this.staticText = effect.staticText; + this.effectType = effect.effectType; this.targetPointer = effect.targetPointer.copy(); if (effect.values != null) { values = new HashMap<>(); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index dae0698619..a346270c3a 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1281,9 +1281,6 @@ public abstract class PlayerImpl implements Player, Serializable { if (Zone.GRAVEYARD == zone && canPlayCardsFromGraveyard()) { for (ActivatedAbility ability : candidateAbilites.getPlayableAbilities(Zone.HAND)) { if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { - if (ability.getManaCosts().isEmpty() && ability.getCosts().isEmpty() && ability instanceof SpellAbility) { - continue; // You can't play spells from graveyard that have no costs - } if (ability.canActivate(playerId, game)) { output.put(ability.getId(), ability); } @@ -1293,9 +1290,6 @@ public abstract class PlayerImpl implements Player, Serializable { if (zone != Zone.BATTLEFIELD && game.getContinuousEffects().asThough(object.getId(), AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, this.getId(), game)) { for (Ability ability : candidateAbilites) { if (canUse || ability.getAbilityType() == AbilityType.SPECIAL_ACTION) { - if (ability.getManaCosts().isEmpty() && ability.getCosts().isEmpty() && ability instanceof SpellAbility && !(Objects.equals(ability.getSourceId(), getCastSourceIdWithAlternateMana()))) { - continue; // You can't play spells that have no costs, unless you can play them without paying their mana costs - } ability.setControllerId(this.getId()); if (ability instanceof ActivatedAbility && ability.getZone().match(Zone.HAND) && ((ActivatedAbility) ability).canActivate(playerId, game)) { diff --git a/Utils/gen-list-implemented-cards-for-set.pl b/Utils/gen-list-implemented-cards-for-set.pl index 2e330ae0f4..664b6077e5 100755 --- a/Utils/gen-list-implemented-cards-for-set.pl +++ b/Utils/gen-list-implemented-cards-for-set.pl @@ -67,7 +67,7 @@ my $toPrint = ''; foreach my $card (sort cardSort @setCards) { my $className = toCamelCase(@{$card}[0]); - my $currentFileName = "../Mage.Sets/src/mage/sets/" . $knownSets{$setName} . "/" . $className . ".java"; + my $currentFileName = "../Mage.Sets/src/mage/cards/" . lc(substr($className, 0, 1)) . "/" . $className . ".java"; if (-e $currentFileName) { if ($toPrint) { $toPrint .= "\n"; diff --git a/Utils/gen-list-unimplemented-cards-for-set.pl b/Utils/gen-list-unimplemented-cards-for-set.pl index a01e5662e8..15df889f21 100755 --- a/Utils/gen-list-unimplemented-cards-for-set.pl +++ b/Utils/gen-list-unimplemented-cards-for-set.pl @@ -94,7 +94,7 @@ foreach my $card (sort cardSort @setCards) { $cardNames {@{$card}[0]} = 1; - my $currentFileName = "../Mage.Sets/src/mage/sets/" . $knownSets{$setName} . "/" . $className . ".java"; + my $currentFileName = "../Mage.Sets/src/mage/cards/" . lc(substr($className, 0, 1)) . "/" . $className . ".java"; if(! -e $currentFileName) { $cardNames {@{$card}[0]} = 0; if ($toPrint) {