From f007ebb289031330c95eaef8163b4d9063bd7483 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Wed, 3 Dec 2025 18:36:03 -0500 Subject: [PATCH 1/4] implement waterbending mechanic --- .../mage/cards/b/BenevolentRiverSpirit.java | 6 +++ Mage.Sets/src/mage/cards/c/CrashingWave.java | 4 +- .../src/mage/cards/f/FoggySwampVisions.java | 3 +- .../src/mage/cards/h/HamaTheBloodbender.java | 15 +++--- .../mage/cards/k/KataraWaterTribesHope.java | 4 +- Mage.Sets/src/mage/cards/w/WaterWhip.java | 6 +++ .../mage/cards/w/WaterbendersRestoration.java | 4 +- .../src/mage/sets/AvatarTheLastAirbender.java | 6 --- .../sets/AvatarTheLastAirbenderEternal.java | 7 --- .../main/java/mage/abilities/AbilityImpl.java | 49 ++++++++++++++++--- .../abilities/costs/common/WaterbendCost.java | 35 ++++++------- .../costs/common/WaterbendXCost.java | 40 +++++++++++++++ .../abilities/costs/mana/ManaCostsImpl.java | 35 +++++++++++++ .../main/java/mage/filter/StaticFilters.java | 11 +++++ 14 files changed, 170 insertions(+), 55 deletions(-) create mode 100644 Mage/src/main/java/mage/abilities/costs/common/WaterbendXCost.java diff --git a/Mage.Sets/src/mage/cards/b/BenevolentRiverSpirit.java b/Mage.Sets/src/mage/cards/b/BenevolentRiverSpirit.java index 25038e78824..8302c8ec223 100644 --- a/Mage.Sets/src/mage/cards/b/BenevolentRiverSpirit.java +++ b/Mage.Sets/src/mage/cards/b/BenevolentRiverSpirit.java @@ -2,8 +2,10 @@ package mage.cards.b; import mage.MageInt; import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.WaterbendCost; import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.keyword.ScryEffect; import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.WardAbility; @@ -11,6 +13,7 @@ import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; +import mage.constants.Zone; import java.util.UUID; @@ -28,6 +31,9 @@ public final class BenevolentRiverSpirit extends CardImpl { // As an additional cost to cast this spell, waterbend {5}. this.getSpellAbility().addCost(new WaterbendCost(5)); + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new InfoEffect("as an additional cost to cast this spell, waterbend {5}") + )); // Flying this.addAbility(FlyingAbility.getInstance()); diff --git a/Mage.Sets/src/mage/cards/c/CrashingWave.java b/Mage.Sets/src/mage/cards/c/CrashingWave.java index e3695a1bebb..0b29c86770e 100644 --- a/Mage.Sets/src/mage/cards/c/CrashingWave.java +++ b/Mage.Sets/src/mage/cards/c/CrashingWave.java @@ -1,7 +1,7 @@ package mage.cards.c; import mage.abilities.Ability; -import mage.abilities.costs.common.WaterbendCost; +import mage.abilities.costs.common.WaterbendXCost; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.TapTargetEffect; import mage.cards.CardImpl; @@ -30,7 +30,7 @@ public final class CrashingWave extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{U}{U}"); // As an additional cost to cast this spell, waterbend {X}. - this.getSpellAbility().addCost(new WaterbendCost("{X}")); + this.getSpellAbility().addCost(new WaterbendXCost()); // Tap up to X target creatures, then distribute three stun counters among tapped creatures your opponents control. this.getSpellAbility().addEffect(new TapTargetEffect("tap up to X target creatures")); diff --git a/Mage.Sets/src/mage/cards/f/FoggySwampVisions.java b/Mage.Sets/src/mage/cards/f/FoggySwampVisions.java index f1ad9b03694..81ba443a62f 100644 --- a/Mage.Sets/src/mage/cards/f/FoggySwampVisions.java +++ b/Mage.Sets/src/mage/cards/f/FoggySwampVisions.java @@ -3,6 +3,7 @@ package mage.cards.f; import mage.abilities.Ability; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; import mage.abilities.costs.common.WaterbendCost; +import mage.abilities.costs.common.WaterbendXCost; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; import mage.abilities.effects.common.SacrificeTargetEffect; @@ -33,7 +34,7 @@ public final class FoggySwampVisions extends CardImpl { super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}{B}"); // As an additional cost to cast this spell, waterbend {X}. - this.getSpellAbility().addCost(new WaterbendCost("{X}")); + this.getSpellAbility().addCost(new WaterbendXCost()); // Exile X target creature cards from graveyards. For each creature card exiled this way, create a token that's a copy of it. At the beginning of your next end step, sacrifice those tokens. this.getSpellAbility().addEffect(new FoggySwampVisionsEffect()); diff --git a/Mage.Sets/src/mage/cards/h/HamaTheBloodbender.java b/Mage.Sets/src/mage/cards/h/HamaTheBloodbender.java index 034e8a07f6e..03786bc5c92 100644 --- a/Mage.Sets/src/mage/cards/h/HamaTheBloodbender.java +++ b/Mage.Sets/src/mage/cards/h/HamaTheBloodbender.java @@ -3,10 +3,10 @@ package mage.cards.h; import mage.MageInt; import mage.abilities.Ability; import mage.abilities.common.EntersBattlefieldTriggeredAbility; -import mage.abilities.costs.Cost; -import mage.abilities.costs.Costs; -import mage.abilities.costs.CostsImpl; import mage.abilities.costs.common.WaterbendCost; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.effects.AsThoughEffectImpl; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.MillCardsTargetEffect; @@ -20,6 +20,7 @@ import mage.game.Game; import mage.players.Player; import mage.target.TargetCard; import mage.target.common.TargetCardInGraveyard; +import mage.target.common.TargetOpponent; import mage.target.targetpointer.FixedTarget; import mage.util.CardUtil; @@ -42,6 +43,7 @@ public final class HamaTheBloodbender extends CardImpl { // When Hama enters, target opponent mills three cards. Exile up to one noncreature, nonland card from that player's graveyard. For as long as you control Hama, you may cast the exiled card during your turn by waterbending {X} rather than paying its mana cost, where X is its mana value. Ability ability = new EntersBattlefieldTriggeredAbility(new MillCardsTargetEffect(3)); ability.addEffect(new HamaTheBloodbenderExileEffect()); + ability.addTarget(new TargetOpponent()); this.addAbility(ability); } @@ -143,10 +145,9 @@ class HamaTheBloodbenderCastEffect extends AsThoughEffectImpl { if (player == null) { return false; } - Costs newCosts = new CostsImpl<>(); - newCosts.add(new WaterbendCost(card.getManaValue())); - newCosts.addAll(card.getSpellAbility().getCosts()); - player.setCastSourceIdWithAlternateMana(card.getId(), null, newCosts); + ManaCosts manaCosts = new ManaCostsImpl<>("{0}"); + manaCosts.add(new WaterbendCost(card.getManaValue())); + player.setCastSourceIdWithAlternateMana(card.getId(), manaCosts, card.getSpellAbility().getCosts()); return true; } } diff --git a/Mage.Sets/src/mage/cards/k/KataraWaterTribesHope.java b/Mage.Sets/src/mage/cards/k/KataraWaterTribesHope.java index d637cb6b178..572857358d2 100644 --- a/Mage.Sets/src/mage/cards/k/KataraWaterTribesHope.java +++ b/Mage.Sets/src/mage/cards/k/KataraWaterTribesHope.java @@ -5,7 +5,7 @@ import mage.abilities.Ability; import mage.abilities.common.ActivateIfConditionActivatedAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MyTurnCondition; -import mage.abilities.costs.common.WaterbendCost; +import mage.abilities.costs.common.WaterbendXCost; import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.common.CreateTokenEffect; @@ -49,7 +49,7 @@ public final class KataraWaterTribesHope extends CardImpl { Ability ability = new ActivateIfConditionActivatedAbility(new SetBasePowerToughnessAllEffect( GetXValue.instance, GetXValue.instance, Duration.EndOfTurn, StaticFilters.FILTER_CONTROLLED_CREATURES - ), new WaterbendCost("{X}"), MyTurnCondition.instance); + ), new WaterbendXCost(), MyTurnCondition.instance); ability.addEffect(new InfoEffect("X can't be 0")); CardUtil.castStream(ability.getCosts(), VariableManaCost.class).forEach(cost -> cost.setMinX(1)); this.addAbility(ability); diff --git a/Mage.Sets/src/mage/cards/w/WaterWhip.java b/Mage.Sets/src/mage/cards/w/WaterWhip.java index 7bf6ea5f9fe..204e3e59992 100644 --- a/Mage.Sets/src/mage/cards/w/WaterWhip.java +++ b/Mage.Sets/src/mage/cards/w/WaterWhip.java @@ -1,12 +1,15 @@ package mage.cards.w; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.WaterbendCost; import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; +import mage.constants.Zone; import mage.target.common.TargetCreaturePermanent; import java.util.UUID; @@ -23,6 +26,9 @@ public final class WaterWhip extends CardImpl { // As an additional cost to cast this spell, waterbend {5}. this.getSpellAbility().addCost(new WaterbendCost(5)); + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new InfoEffect("as an additional cost to cast this spell, waterbend {5}") + )); // Return up to two target creatures to their owners' hands. Draw two cards. this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/w/WaterbendersRestoration.java b/Mage.Sets/src/mage/cards/w/WaterbendersRestoration.java index a2f1cf39bed..34a6b9cf34d 100644 --- a/Mage.Sets/src/mage/cards/w/WaterbendersRestoration.java +++ b/Mage.Sets/src/mage/cards/w/WaterbendersRestoration.java @@ -1,6 +1,6 @@ package mage.cards.w; -import mage.abilities.costs.common.WaterbendCost; +import mage.abilities.costs.common.WaterbendXCost; import mage.abilities.effects.common.ExileReturnBattlefieldNextEndStepTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; @@ -22,7 +22,7 @@ public final class WaterbendersRestoration extends CardImpl { this.subtype.add(SubType.LESSON); // As an additional cost to cast this spell, waterbend {X}. - this.getSpellAbility().addCost(new WaterbendCost("{X}")); + this.getSpellAbility().addCost(new WaterbendXCost()); // Exile X target creatures you control. Return those cards to the battlefield under their owner's control at the beginning of the next end step. this.getSpellAbility().addEffect(new ExileReturnBattlefieldNextEndStepTargetEffect() diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java index 1cc47ff9291..c930478e179 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbender.java @@ -4,15 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class AvatarTheLastAirbender extends ExpansionSet { - private static final List unfinished = Arrays.asList("Aang's Iceberg", "Aang, Swift Savior", "Avatar Aang", "Benevolent River Spirit", "Crashing Wave", "Flexible Waterbender", "Foggy Swamp Vinebender", "Foggy Swamp Visions", "Geyser Leaper", "Giant Koi", "Hama, the Bloodbender", "Invasion Submersible", "Katara, Bending Prodigy", "Katara, Water Tribe's Hope", "North Pole Patrol", "Ruinous Waterbending", "Secret of Bloodbending", "Spirit Water Revival", "The Legend of Kuruk", "The Unagi of Kyoshi Island", "Waterbender Ascension", "Waterbending Lesson", "Water Tribe Rallier", "Watery Grasp", "Yue, the Moon Spirit"); private static final AvatarTheLastAirbender instance = new AvatarTheLastAirbender(); public static AvatarTheLastAirbender getInstance() { @@ -430,7 +426,5 @@ public final class AvatarTheLastAirbender extends ExpansionSet { cards.add(new SetCardInfo("Zuko, Conflicted", 253, Rarity.RARE, mage.cards.z.ZukoConflicted.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Zuko, Conflicted", 302, Rarity.RARE, mage.cards.z.ZukoConflicted.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Zuko, Exiled Prince", 163, Rarity.UNCOMMON, mage.cards.z.ZukoExiledPrince.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); } } diff --git a/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java b/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java index e5f25cf5dd7..a7b26312367 100644 --- a/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java +++ b/Mage.Sets/src/mage/sets/AvatarTheLastAirbenderEternal.java @@ -4,16 +4,11 @@ import mage.cards.ExpansionSet; import mage.constants.Rarity; import mage.constants.SetType; -import java.util.Arrays; -import java.util.List; - /** * @author TheElk801 */ public final class AvatarTheLastAirbenderEternal extends ExpansionSet { - private static final List unfinished = Arrays.asList("Katara, Seeking Revenge", "Ruthless Waterbender", "Waterbender's Restoration", "Water Whip"); - private static final AvatarTheLastAirbenderEternal instance = new AvatarTheLastAirbenderEternal(); public static AvatarTheLastAirbenderEternal getInstance() { @@ -342,7 +337,5 @@ public final class AvatarTheLastAirbenderEternal extends ExpansionSet { cards.add(new SetCardInfo("Zuko, Firebending Master", 127, Rarity.MYTHIC, mage.cards.z.ZukoFirebendingMaster.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Zuko, Firebending Master", 200, Rarity.MYTHIC, mage.cards.z.ZukoFirebendingMaster.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Zuko, Seeking Honor", 150, Rarity.UNCOMMON, mage.cards.z.ZukoSeekingHonor.class)); - - cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName())); } } diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index af9c1ec624e..bbf51847206 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -7,10 +7,8 @@ import mage.abilities.common.EntersBattlefieldAbility; import mage.abilities.condition.Condition; import mage.abilities.costs.*; import mage.abilities.costs.common.PayLifeCost; -import mage.abilities.costs.mana.ManaCost; -import mage.abilities.costs.mana.ManaCosts; -import mage.abilities.costs.mana.ManaCostsImpl; -import mage.abilities.costs.mana.VariableManaCost; +import mage.abilities.costs.common.WaterbendCost; +import mage.abilities.costs.mana.*; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; import mage.abilities.effects.Effects; @@ -25,6 +23,7 @@ import mage.choices.ChoiceHintType; import mage.choices.ChoiceImpl; import mage.constants.*; import mage.filter.FilterMana; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.command.Dungeon; import mage.game.command.Emblem; @@ -39,8 +38,10 @@ import mage.game.stack.StackAbility; import mage.players.Player; import mage.target.Target; import mage.target.TargetCard; +import mage.target.TargetPermanent; import mage.target.Targets; import mage.target.common.TargetCardInLibrary; +import mage.target.common.TargetControlledPermanent; import mage.target.targetadjustment.GenericTargetAdjuster; import mage.target.targetadjustment.TargetAdjuster; import mage.util.CardUtil; @@ -50,6 +51,7 @@ import mage.watchers.Watcher; import org.apache.log4j.Logger; import java.util.*; +import java.util.stream.Collectors; /** * @author BetaSteward_at_googlemail.com @@ -349,6 +351,7 @@ public abstract class AbilityImpl implements Ability { // Phyrexian mana symbols, the player announces whether they intend to pay 2 // life or the corresponding colored mana cost for each of those symbols. AbilityImpl.handlePhyrexianCosts(game, this, this, this.getManaCostsToPay()); + AbilityImpl.handleWaterbendingCosts(game, this, this, this.getManaCostsToPay()); // 20241022 - 601.2b // Not yet included in 601.2b but this is where it will be @@ -687,6 +690,38 @@ public abstract class AbilityImpl implements Ability { } } + public static void handleWaterbendingCosts(Game game, Ability source, Ability abilityToPay, ManaCosts manaCostsToPay) { + Player controller = game.getPlayer(source.getControllerId()); + if (controller == null) { + return; + } + + int total = CardUtil + .castStream(manaCostsToPay, WaterbendCost.class) + .mapToInt(WaterbendCost::manaValue) + .sum(); + if (total < 1) { + return; + } + TargetPermanent target = new TargetControlledPermanent( + 0, total, StaticFilters.FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE, true + ); + target.withChooseHint("to tap for waterbending"); + controller.choose(Outcome.Tap, target, source, game); + Set permanents = target + .getTargets() + .stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + for (Permanent permanent : permanents) { + permanent.tap(source, game); + } + manaCostsToPay.removeIf(WaterbendCost.class::isInstance); + abilityToPay.addCost(new GenericManaCost(total - permanents.size())); + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.WATERBENDED, source.getSourceId(), source, controller.getId(), total)); + } + /** * Prepare and pay Phyrexian style effects like replace mana by life * Must be called after original Phyrexian mana processing and after cost modifications, e.g. on payment @@ -1701,15 +1736,15 @@ public abstract class AbilityImpl implements Ability { @Override public void initSourceObjectZoneChangeCounter(Game game, boolean force) { - if (!(this instanceof MageSingleton) && (force || sourceObjectZoneChangeCounter == 0 )) { + if (!(this instanceof MageSingleton) && (force || sourceObjectZoneChangeCounter == 0)) { setSourceObjectZoneChangeCounter(getCurrentSourceObjectZoneChangeCounter(game)); } } - private int getCurrentSourceObjectZoneChangeCounter(Game game){ + private int getCurrentSourceObjectZoneChangeCounter(Game game) { int zcc = game.getState().getZoneChangeCounter(getSourceId()); Permanent p = game.getPermanentEntering(getSourceId()); - if (p != null && !(p instanceof PermanentToken)){ + if (p != null && !(p instanceof PermanentToken)) { // If the triggered ability triggered while the permanent is entering the battlefield // then add 1 zcc so that it triggers as if the permanent was already on the battlefield // So "Enters with counters" causes "Whenever counters are placed" to trigger with battlefield zcc diff --git a/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java b/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java index db762f8a0cd..1b350bbc2da 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java @@ -1,26 +1,24 @@ package mage.abilities.costs.common; -import mage.abilities.Ability; -import mage.abilities.costs.Cost; -import mage.abilities.costs.CostImpl; -import mage.game.Game; - -import java.util.UUID; +import mage.abilities.costs.mana.GenericManaCost; /** - * TODO: Implement properly + * 701.67. Waterbend + *

+ * 701.67a “Waterbend [cost]” means “Pay [cost]. For each generic mana in that cost, + * you may tap an untapped artifact or creature you control rather than pay that mana.” + *

+ * 701.67b If a waterbend cost is part of the total cost to cast a spell or activate an ability + * (usually because the waterbend cost itself is an additional cost), the alternate method to pay for mana + * described in rule 701.67a may be used only to pay for the amount of generic mana in the waterbend cost, + * even if the total cost to cast that spell or activate that ability includes other generic mana components. * * @author TheElk801 */ -public class WaterbendCost extends CostImpl { +public class WaterbendCost extends GenericManaCost { public WaterbendCost(int amount) { - this("{" + amount + '}'); - } - - public WaterbendCost(String mana) { - super(); - this.text = "waterbend " + mana; + super(amount); } private WaterbendCost(final WaterbendCost cost) { @@ -33,12 +31,7 @@ public class WaterbendCost extends CostImpl { } @Override - public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return false; - } - - @Override - public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { - return false; + public String getText() { + return "waterbend " + super.getText(); } } diff --git a/Mage/src/main/java/mage/abilities/costs/common/WaterbendXCost.java b/Mage/src/main/java/mage/abilities/costs/common/WaterbendXCost.java new file mode 100644 index 00000000000..f0b5ceda762 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/costs/common/WaterbendXCost.java @@ -0,0 +1,40 @@ +package mage.abilities.costs.common; + +import mage.abilities.Ability; +import mage.abilities.costs.Cost; +import mage.abilities.costs.CostImpl; +import mage.game.Game; + +import java.util.UUID; + +/** + * TODO: Implement properly + * + * @author TheElk801 + */ +public class WaterbendXCost extends CostImpl { + + public WaterbendXCost() { + super(); + this.text = "waterbend {X}"; + } + + private WaterbendXCost(final WaterbendXCost cost) { + super(cost); + } + + @Override + public WaterbendXCost copy() { + return new WaterbendXCost(this); + } + + @Override + public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { + return false; + } + + @Override + public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { + return false; + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java index 566934b34c7..1b81566e238 100644 --- a/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java +++ b/Mage/src/main/java/mage/abilities/costs/mana/ManaCostsImpl.java @@ -5,20 +5,27 @@ import mage.abilities.Ability; import mage.abilities.AbilityImpl; import mage.abilities.costs.*; import mage.abilities.costs.common.PayLifeCost; +import mage.abilities.costs.common.WaterbendCost; import mage.abilities.mana.ManaOptions; import mage.constants.ColoredManaSymbol; import mage.constants.ManaType; import mage.constants.Outcome; import mage.filter.Filter; +import mage.filter.StaticFilters; import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; import mage.players.ManaPool; import mage.players.Player; +import mage.target.TargetPermanent; import mage.target.Targets; +import mage.target.common.TargetControlledPermanent; import mage.util.CardUtil; import mage.util.ManaUtil; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; /** * @param @@ -162,6 +169,7 @@ public class ManaCostsImpl extends ArrayList implements M if (payingPlayer != null) { int bookmark = game.bookmarkState(); handlePhyrexianManaCosts(ability, payingPlayer, source, game); + handleWaterbendingCosts(ability, payingPlayer, source, game); if (pay(ability, game, source, payingPlayerId, false, null)) { game.removeBookmark(bookmark); return true; @@ -195,6 +203,33 @@ public class ManaCostsImpl extends ArrayList implements M tempCosts.pay(source, game, source, payingPlayer.getId(), false, null); } + private void handleWaterbendingCosts(Ability abilityToPay, Player payingPlayer, Ability source, Game game) { + int total = CardUtil + .castStream(this, WaterbendCost.class) + .mapToInt(WaterbendCost::manaValue) + .sum(); + if (total < 1) { + return; + } + TargetPermanent target = new TargetControlledPermanent( + 0, total, StaticFilters.FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE, true + ); + target.withChooseHint("to tap for waterbending"); + payingPlayer.choose(Outcome.Tap, target, source, game); + Set permanents = target + .getTargets() + .stream() + .map(game::getPermanent) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + for (Permanent permanent : permanents) { + permanent.tap(source, game); + } + this.removeIf(WaterbendCost.class::isInstance); + this.add(new GenericManaCost(total - permanents.size())); + game.fireEvent(GameEvent.getEvent(GameEvent.EventType.WATERBENDED, source.getSourceId(), source, payingPlayer.getId(), total)); + } + @Override public ManaCosts getUnpaid() { ManaCosts unpaid = new ManaCostsImpl<>(); diff --git a/Mage/src/main/java/mage/filter/StaticFilters.java b/Mage/src/main/java/mage/filter/StaticFilters.java index 83ff2fb5d20..fd391577bbb 100644 --- a/Mage/src/main/java/mage/filter/StaticFilters.java +++ b/Mage/src/main/java/mage/filter/StaticFilters.java @@ -477,6 +477,17 @@ public final class StaticFilters { FILTER_CONTROLLED_PERMANENT_ARTIFACT_OR_CREATURE.setLockedFilter(true); } + public static final FilterControlledPermanent FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE = new FilterControlledPermanent("untapped artifact or creature you control"); + + static { + FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE.add(TappedPredicate.UNTAPPED); + FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate() + )); + FILTER_CONTROLLED_UNTAPPED_ARTIFACT_OR_CREATURE.setLockedFilter(true); + } + public static final FilterControlledPermanent FILTER_CONTROLLED_ARTIFACT_OR_OTHER_CREATURE = new FilterControlledPermanent("another creature or an artifact"); static { From 4480dbdf4db3722d747ea8afb1c2b8510c800fca Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 4 Dec 2025 10:12:52 -0500 Subject: [PATCH 2/4] properly implement WaterbendXCost --- .../mage/cards/b/BenevolentRiverSpirit.java | 2 +- Mage.Sets/src/mage/cards/c/CrashingWave.java | 8 +- .../src/mage/cards/f/FoggySwampVisions.java | 6 +- .../mage/cards/k/KataraWaterTribesHope.java | 5 +- Mage.Sets/src/mage/cards/w/WaterWhip.java | 2 +- .../mage/cards/w/WaterbendersRestoration.java | 6 ++ .../main/java/mage/abilities/AbilityImpl.java | 100 ++++++++++-------- .../abilities/costs/common/WaterbendCost.java | 3 + .../costs/common/WaterbendXCost.java | 30 +++--- 9 files changed, 90 insertions(+), 72 deletions(-) diff --git a/Mage.Sets/src/mage/cards/b/BenevolentRiverSpirit.java b/Mage.Sets/src/mage/cards/b/BenevolentRiverSpirit.java index 8302c8ec223..6a4985504b2 100644 --- a/Mage.Sets/src/mage/cards/b/BenevolentRiverSpirit.java +++ b/Mage.Sets/src/mage/cards/b/BenevolentRiverSpirit.java @@ -33,7 +33,7 @@ public final class BenevolentRiverSpirit extends CardImpl { this.getSpellAbility().addCost(new WaterbendCost(5)); this.addAbility(new SimpleStaticAbility( Zone.ALL, new InfoEffect("as an additional cost to cast this spell, waterbend {5}") - )); + ).setRuleAtTheTop(true)); // Flying this.addAbility(FlyingAbility.getInstance()); diff --git a/Mage.Sets/src/mage/cards/c/CrashingWave.java b/Mage.Sets/src/mage/cards/c/CrashingWave.java index 0b29c86770e..f4815dd3d1e 100644 --- a/Mage.Sets/src/mage/cards/c/CrashingWave.java +++ b/Mage.Sets/src/mage/cards/c/CrashingWave.java @@ -1,13 +1,16 @@ package mage.cards.c; import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.WaterbendXCost; import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.TapTargetEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.Outcome; +import mage.constants.Zone; import mage.counters.CounterType; import mage.filter.FilterPermanent; import mage.filter.common.FilterOpponentsCreaturePermanent; @@ -31,6 +34,9 @@ public final class CrashingWave extends CardImpl { // As an additional cost to cast this spell, waterbend {X}. this.getSpellAbility().addCost(new WaterbendXCost()); + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new InfoEffect("as an additional cost to cast this spell, waterbend {X}") + ).setRuleAtTheTop(true)); // Tap up to X target creatures, then distribute three stun counters among tapped creatures your opponents control. this.getSpellAbility().addEffect(new TapTargetEffect("tap up to X target creatures")); @@ -79,7 +85,7 @@ class CrashingWaveEffect extends OneShotEffect { } TargetPermanentAmount target = new TargetPermanentAmount(3, 1, filter); target.withNotTarget(true); - player.chooseTarget(outcome, target, source, game); + target.chooseTarget(outcome, player.getId(), source, game); for (UUID targetId : target.getTargets()) { Optional.ofNullable(targetId) .map(game::getPermanent) diff --git a/Mage.Sets/src/mage/cards/f/FoggySwampVisions.java b/Mage.Sets/src/mage/cards/f/FoggySwampVisions.java index 81ba443a62f..72ac7cb4022 100644 --- a/Mage.Sets/src/mage/cards/f/FoggySwampVisions.java +++ b/Mage.Sets/src/mage/cards/f/FoggySwampVisions.java @@ -1,11 +1,12 @@ package mage.cards.f; import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; -import mage.abilities.costs.common.WaterbendCost; import mage.abilities.costs.common.WaterbendXCost; import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect; +import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.SacrificeTargetEffect; import mage.cards.*; import mage.constants.CardType; @@ -35,6 +36,9 @@ public final class FoggySwampVisions extends CardImpl { // As an additional cost to cast this spell, waterbend {X}. this.getSpellAbility().addCost(new WaterbendXCost()); + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new InfoEffect("as an additional cost to cast this spell, waterbend {X}") + ).setRuleAtTheTop(true)); // Exile X target creature cards from graveyards. For each creature card exiled this way, create a token that's a copy of it. At the beginning of your next end step, sacrifice those tokens. this.getSpellAbility().addEffect(new FoggySwampVisionsEffect()); diff --git a/Mage.Sets/src/mage/cards/k/KataraWaterTribesHope.java b/Mage.Sets/src/mage/cards/k/KataraWaterTribesHope.java index 572857358d2..c8e25cc4cf0 100644 --- a/Mage.Sets/src/mage/cards/k/KataraWaterTribesHope.java +++ b/Mage.Sets/src/mage/cards/k/KataraWaterTribesHope.java @@ -6,7 +6,6 @@ import mage.abilities.common.ActivateIfConditionActivatedAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.condition.common.MyTurnCondition; import mage.abilities.costs.common.WaterbendXCost; -import mage.abilities.costs.mana.VariableManaCost; import mage.abilities.dynamicvalue.common.GetXValue; import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.InfoEffect; @@ -20,7 +19,6 @@ import mage.constants.SubType; import mage.constants.SuperType; import mage.filter.StaticFilters; import mage.game.permanent.token.AllyToken; -import mage.util.CardUtil; import java.util.UUID; @@ -49,9 +47,8 @@ public final class KataraWaterTribesHope extends CardImpl { Ability ability = new ActivateIfConditionActivatedAbility(new SetBasePowerToughnessAllEffect( GetXValue.instance, GetXValue.instance, Duration.EndOfTurn, StaticFilters.FILTER_CONTROLLED_CREATURES - ), new WaterbendXCost(), MyTurnCondition.instance); + ), new WaterbendXCost(1), MyTurnCondition.instance); ability.addEffect(new InfoEffect("X can't be 0")); - CardUtil.castStream(ability.getCosts(), VariableManaCost.class).forEach(cost -> cost.setMinX(1)); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/w/WaterWhip.java b/Mage.Sets/src/mage/cards/w/WaterWhip.java index 204e3e59992..5917a472da4 100644 --- a/Mage.Sets/src/mage/cards/w/WaterWhip.java +++ b/Mage.Sets/src/mage/cards/w/WaterWhip.java @@ -28,7 +28,7 @@ public final class WaterWhip extends CardImpl { this.getSpellAbility().addCost(new WaterbendCost(5)); this.addAbility(new SimpleStaticAbility( Zone.ALL, new InfoEffect("as an additional cost to cast this spell, waterbend {5}") - )); + ).setRuleAtTheTop(true)); // Return up to two target creatures to their owners' hands. Draw two cards. this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); diff --git a/Mage.Sets/src/mage/cards/w/WaterbendersRestoration.java b/Mage.Sets/src/mage/cards/w/WaterbendersRestoration.java index 34a6b9cf34d..a8ae100f9f5 100644 --- a/Mage.Sets/src/mage/cards/w/WaterbendersRestoration.java +++ b/Mage.Sets/src/mage/cards/w/WaterbendersRestoration.java @@ -1,11 +1,14 @@ package mage.cards.w; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.costs.common.WaterbendXCost; import mage.abilities.effects.common.ExileReturnBattlefieldNextEndStepTargetEffect; +import mage.abilities.effects.common.InfoEffect; import mage.cards.CardImpl; import mage.cards.CardSetInfo; import mage.constants.CardType; import mage.constants.SubType; +import mage.constants.Zone; import mage.target.common.TargetControlledCreaturePermanent; import mage.target.targetadjustment.XTargetsCountAdjuster; @@ -23,6 +26,9 @@ public final class WaterbendersRestoration extends CardImpl { // As an additional cost to cast this spell, waterbend {X}. this.getSpellAbility().addCost(new WaterbendXCost()); + this.addAbility(new SimpleStaticAbility( + Zone.ALL, new InfoEffect("as an additional cost to cast this spell, waterbend {X}") + ).setRuleAtTheTop(true)); // Exile X target creatures you control. Return those cards to the battlefield under their owner's control at the beginning of the next end step. this.getSpellAbility().addEffect(new ExileReturnBattlefieldNextEndStepTargetEffect() diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index bbf51847206..4d0f3cec930 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -8,6 +8,7 @@ import mage.abilities.condition.Condition; import mage.abilities.costs.*; import mage.abilities.costs.common.PayLifeCost; import mage.abilities.costs.common.WaterbendCost; +import mage.abilities.costs.common.WaterbendXCost; import mage.abilities.costs.mana.*; import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.Effect; @@ -814,54 +815,59 @@ public abstract class AbilityImpl implements Ability { } } } - if (variableManaCost != null) { - if (!variableManaCost.isPaid()) { // should only happen for human players - int xValue; - if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) { - if (variableManaCost.wasAnnounced()) { - // announce by rules - xValue = variableManaCost.getAmount(); - } else { - // announce by player - xValue = controller.announceX(variableManaCost.getMinX(), variableManaCost.getMaxX(), - "Announce the value for " + variableManaCost.getText(), game, this, true); - } - - int amountMana = xValue * variableManaCost.getXInstancesCount(); - StringBuilder manaString = threadLocalBuilder.get(); - if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) { - manaString.append('{').append(amountMana).append('}'); - } else { - String manaSymbol = null; - if (variableManaCost.getFilter().isBlack()) { - if (variableManaCost.getFilter().isRed()) { - manaSymbol = "B/R"; - } else { - manaSymbol = "B"; - } - } else if (variableManaCost.getFilter().isRed()) { - manaSymbol = "R"; - } else if (variableManaCost.getFilter().isBlue()) { - manaSymbol = "U"; - } else if (variableManaCost.getFilter().isGreen()) { - manaSymbol = "G"; - } else if (variableManaCost.getFilter().isWhite()) { - manaSymbol = "W"; - } - if (manaSymbol == null) { - throw new UnsupportedOperationException("ManaFilter is not supported: " + this); - } - for (int i = 0; i < amountMana; i++) { - manaString.append('{').append(manaSymbol).append('}'); - } - } - addManaCostsToPay(new ManaCostsImpl<>(manaString.toString())); - getManaCostsToPay().setX(xValue, amountMana); - setCostsTag("X", xValue); - } - variableManaCost.setPaid(); - } + if (variableManaCost == null) { + return variableManaCost; } + if (variableManaCost.isPaid()) { + return variableManaCost; + } // should only happen for human players + int xValue; + if (!noMana || variableManaCost.getCostType().canUseAnnounceOnFreeCast()) { + if (variableManaCost.wasAnnounced()) { + // announce by rules + xValue = variableManaCost.getAmount(); + } else { + // announce by player + xValue = controller.announceX(variableManaCost.getMinX(), variableManaCost.getMaxX(), + "Announce the value for " + variableManaCost.getText(), game, this, true); + } + + int amountMana = xValue * variableManaCost.getXInstancesCount(); + StringBuilder manaString = threadLocalBuilder.get(); + if (!(variableManaCost instanceof WaterbendXCost)) { + if (variableManaCost.getFilter() == null || variableManaCost.getFilter().isGeneric()) { + manaString.append('{').append(amountMana).append('}'); + } else { + String manaSymbol; + if (variableManaCost.getFilter().isBlack()) { + if (variableManaCost.getFilter().isRed()) { + manaSymbol = "B/R"; + } else { + manaSymbol = "B"; + } + } else if (variableManaCost.getFilter().isRed()) { + manaSymbol = "R"; + } else if (variableManaCost.getFilter().isBlue()) { + manaSymbol = "U"; + } else if (variableManaCost.getFilter().isGreen()) { + manaSymbol = "G"; + } else if (variableManaCost.getFilter().isWhite()) { + manaSymbol = "W"; + } else { + throw new UnsupportedOperationException("ManaFilter is not supported: " + this); + } + for (int i = 0; i < amountMana; i++) { + manaString.append('{').append(manaSymbol).append('}'); + } + } + addManaCostsToPay(new ManaCostsImpl<>(manaString.toString())); + } else { + addManaCostsToPay(new WaterbendCost(amountMana)); + } + getManaCostsToPay().setX(xValue, amountMana); + setCostsTag("X", xValue); + } + variableManaCost.setPaid(); return variableManaCost; } diff --git a/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java b/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java index 1b350bbc2da..98605bc23f0 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java @@ -12,6 +12,9 @@ import mage.abilities.costs.mana.GenericManaCost; * (usually because the waterbend cost itself is an additional cost), the alternate method to pay for mana * described in rule 701.67a may be used only to pay for the amount of generic mana in the waterbend cost, * even if the total cost to cast that spell or activate that ability includes other generic mana components. + *

+ * If you need Waterbend {X} then use {@link WaterbendXCost} + * If using as an additional cost for a spell, add an ability with an InfoEffect for proper text generation (see WaterWhip) * * @author TheElk801 */ diff --git a/Mage/src/main/java/mage/abilities/costs/common/WaterbendXCost.java b/Mage/src/main/java/mage/abilities/costs/common/WaterbendXCost.java index f0b5ceda762..b9d2e4d25a8 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/WaterbendXCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/WaterbendXCost.java @@ -1,22 +1,23 @@ package mage.abilities.costs.common; -import mage.abilities.Ability; -import mage.abilities.costs.Cost; -import mage.abilities.costs.CostImpl; -import mage.game.Game; - -import java.util.UUID; +import mage.abilities.costs.VariableCostType; +import mage.abilities.costs.mana.VariableManaCost; /** - * TODO: Implement properly + * Used for Waterbend {X} costs, otherwise use {@link WaterbendCost} + * If using as an additional cost for a spell, add an ability with an InfoEffect for proper text generation (see WaterbendersRestoration) * * @author TheElk801 */ -public class WaterbendXCost extends CostImpl { +public class WaterbendXCost extends VariableManaCost { public WaterbendXCost() { - super(); - this.text = "waterbend {X}"; + this(0); + } + + public WaterbendXCost(int minX) { + super(VariableCostType.NORMAL); + this.setMinX(minX); } private WaterbendXCost(final WaterbendXCost cost) { @@ -29,12 +30,7 @@ public class WaterbendXCost extends CostImpl { } @Override - public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { - return false; - } - - @Override - public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) { - return false; + public String getText() { + return "waterbend {X}"; } } From a880bb9149a2db02d6a98d30e4c7d33a9eca48e1 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 18 Dec 2025 10:55:47 -0500 Subject: [PATCH 3/4] add tests --- .../mage/test/cards/mana/WaterbendTest.java | 115 ++++++++++++++++ .../abilities/costs/common/WaterbendCost.java | 4 + .../main/java/mage/players/PlayerImpl.java | 126 +++++++++--------- 3 files changed, 182 insertions(+), 63 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/mana/WaterbendTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/mana/WaterbendTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/mana/WaterbendTest.java new file mode 100644 index 00000000000..5e96e299d44 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/mana/WaterbendTest.java @@ -0,0 +1,115 @@ +package org.mage.test.cards.mana; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.player.TestPlayer; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class WaterbendTest extends CardTestPlayerBase { + + private static final String waterbender = "Flexible Waterbender"; + + @Test + public void testJustMana() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.BATTLEFIELD, playerA, waterbender); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "waterbend"); + setChoice(playerA, TestPlayer.CHOICE_SKIP); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, waterbender, 5, 2); + assertTapped(waterbender, false); + assertTapped("Island", true); + } + + private static final String relic = "Darksteel Relic"; + + @Test + public void testNoMana() { + addCard(Zone.BATTLEFIELD, playerA, relic, 2); + addCard(Zone.BATTLEFIELD, playerA, waterbender); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "waterbend"); + setChoice(playerA, waterbender); + setChoice(playerA, relic); + setChoice(playerA, relic); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, waterbender, 5, 2); + assertTapped(waterbender, true); + assertTapped(relic, true); + } + + @Test + public void testManaAndCreature() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.BATTLEFIELD, playerA, waterbender); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "waterbend"); + setChoice(playerA, waterbender); + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, waterbender, 5, 2); + assertTapped(waterbender, true); + assertTapped("Island", true); + } + + private static final String katara = "Katara, Water Tribe's Hope"; + + @Test + public void testX() { + addCard(Zone.BATTLEFIELD, playerA, "Island"); + addCard(Zone.BATTLEFIELD, playerA, relic); + addCard(Zone.BATTLEFIELD, playerA, katara); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "waterbend"); + setChoice(playerA, "X=3"); + setChoice(playerA, relic); + setChoice(playerA, katara); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertPowerToughness(playerA, katara, 3, 3); + assertTapped(relic, true); + assertTapped(katara, true); + assertTapped("Island", true); + } + + private static final String spirit = "Benevolent River Spirit"; + + @Test + public void testSpellCost() { + removeAllCardsFromLibrary(playerA); // removes need to make scry choices + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + addCard(Zone.BATTLEFIELD, playerA, relic, 3); + addCard(Zone.HAND, playerA, spirit); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, spirit); + setChoice(playerA, relic); + setChoice(playerA, relic); + setChoice(playerA, relic); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertTapped("Island", true); + assertTapped(relic, true); + assertTapped(spirit, false); + } +} diff --git a/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java b/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java index 98605bc23f0..fc2c034de9b 100644 --- a/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java +++ b/Mage/src/main/java/mage/abilities/costs/common/WaterbendCost.java @@ -1,5 +1,6 @@ package mage.abilities.costs.common; +import mage.Mana; import mage.abilities.costs.mana.GenericManaCost; /** @@ -22,6 +23,9 @@ public class WaterbendCost extends GenericManaCost { public WaterbendCost(int amount) { super(amount); + for (int i = 0; i < amount; i++) { + options.add(Mana.ColorlessMana(i)); + } } private WaterbendCost(final WaterbendCost cost) { diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index e2c6348cbaa..fdc6ae38fa6 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3711,62 +3711,63 @@ public abstract class PlayerImpl implements Player, Serializable { * @return */ protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { - if (!ability.isManaActivatedAbility()) { - 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; + if (ability.isManaActivatedAbility()) { + return false; + } + 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; + } + + // apply dynamic costs and cost modification + copy.adjustX(game); + if (availableMana != null) { + // TODO: need research, why it look at availableMana here - can delete condition? + game.getContinuousEffects().costModification(copy, game); + } + boolean canBeCastRegularly = true; + Set allowedIdentifiers = null; + if (copy instanceof SpellAbility) { + if (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; + } + allowedIdentifiers = ((SpellAbility) copy).spellCanBeActivatedNow(playerId, game); + if (!allowedIdentifiers.contains(MageIdentifier.Default)) { + // If the timing restriction is lifted only for specific MageIdentifier, the default cast can not be used. + canBeCastRegularly = false; + } + } + if (canBeCastRegularly && canPayMinimumManaCost(copy, availableMana, game)) { + return true; + } + + // ALTERNATIVE COST FROM dynamic effects + for (MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) { + if (allowedIdentifiers != null && !(allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(identifier))) { + continue; + } + ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier); + Costs costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier); + + boolean canPutToPlay = true; + if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { + canPutToPlay = false; + } + if (costs != null && !costs.canPay(copy, copy, playerId, game)) { + canPutToPlay = false; } - // apply dynamic costs and cost modification - copy.adjustX(game); - if (availableMana != null) { - // TODO: need research, why it look at availableMana here - can delete condition? - game.getContinuousEffects().costModification(copy, game); - } - boolean canBeCastRegularly = true; - Set allowedIdentifiers = null; - if (copy instanceof SpellAbility) { - if (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; - } - allowedIdentifiers = ((SpellAbility) copy).spellCanBeActivatedNow(playerId, game); - if (!allowedIdentifiers.contains(MageIdentifier.Default)) { - // If the timing restriction is lifted only for specific MageIdentifier, the default cast can not be used. - canBeCastRegularly = false; - } - } - if (canBeCastRegularly && canPayMinimumManaCost(copy, availableMana, game)) { + if (canPutToPlay) { return true; } + } - // ALTERNATIVE COST FROM dynamic effects - for (MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) { - if (allowedIdentifiers != null && !(allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(identifier))) { - continue; - } - ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier); - Costs costs = getCastSourceIdCosts().get(copy.getSourceId()).get(identifier); - - boolean canPutToPlay = true; - if (alternateCosts != null && !alternateCosts.canPay(copy, copy, playerId, game)) { - canPutToPlay = false; - } - if (costs != null && !costs.canPay(copy, copy, playerId, game)) { - canPutToPlay = false; - } - - if (canPutToPlay) { - return true; - } - } - - // ALTERNATIVE COST from source card (any AlternativeSourceCosts) - if (AbilityType.SPELL.equals(ability.getAbilityType())) { - return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); - } + // ALTERNATIVE COST from source card (any AlternativeSourceCosts) + if (AbilityType.SPELL.equals(ability.getAbilityType())) { + return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game); } return false; } @@ -4110,7 +4111,7 @@ public abstract class PlayerImpl implements Player, Serializable { TransformingDoubleFacedCard mainCard = (TransformingDoubleFacedCard) object; getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(game), availableMana, output); - } else if (object instanceof CardWithSpellOption) { + } else if (object instanceof CardWithSpellOption) { // adventure must use different card characteristics for different spells (main or adventure) CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object; getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption.getSpellCard(), cardWithSpellOption.getSpellCard().getAbilities(game), availableMana, output); @@ -4254,8 +4255,7 @@ public abstract class PlayerImpl implements Player, Serializable { Game game = originalGame.createSimulationForPlayableCalc(); ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition)) - boolean fromAll = fromZone.equals(Zone.ALL); - if (hidden && (fromAll || fromZone == Zone.HAND)) { + if (hidden && fromZone.match(Zone.HAND)) { for (Card card : hand.getCards(game)) { for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) if (ability.getZone().match(Zone.HAND)) { @@ -4300,7 +4300,7 @@ public abstract class PlayerImpl implements Player, Serializable { } } - if (fromAll || fromZone == Zone.GRAVEYARD) { + if (fromZone.match(Zone.GRAVEYARD)) { for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) { Player player = game.getPlayer(playerId); if (player == null) { @@ -4312,7 +4312,7 @@ public abstract class PlayerImpl implements Player, Serializable { } } - if (fromAll || fromZone == Zone.EXILED) { + if (fromZone.match(Zone.EXILED)) { for (ExileZone exile : game.getExile().getExileZones()) { for (Card card : exile.getCards(game)) { getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); @@ -4321,7 +4321,7 @@ public abstract class PlayerImpl implements Player, Serializable { } // check to play revealed cards - if (fromAll) { + if (fromZone.match(Zone.ALL)) { for (Cards revealedCards : game.getState().getRevealed().values()) { for (Card card : revealedCards.getCards(game)) { // revealed cards can be from any zones @@ -4331,7 +4331,7 @@ public abstract class PlayerImpl implements Player, Serializable { } // outside cards - if (fromAll || fromZone == Zone.OUTSIDE) { + if (fromZone.match(Zone.OUTSIDE)) { // companion cards for (Cards companionCards : game.getState().getCompanion().values()) { for (Card card : companionCards.getCards(game)) { @@ -4349,7 +4349,7 @@ public abstract class PlayerImpl implements Player, Serializable { } // check if it's possible to play the top card of a library - if (fromAll || fromZone == Zone.LIBRARY) { + if (fromZone.match(Zone.LIBRARY)) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { Player player = game.getPlayer(playerInRangeId); if (player != null && player.getLibrary().hasCards()) { @@ -4365,7 +4365,7 @@ public abstract class PlayerImpl implements Player, Serializable { // TODO: remove direct hand check (reveal fix in Sen Triplets)? // human games: cards from opponent's hand must be revealed before play // AI games: computer can see and play cards from opponent's hand without reveal - if (fromAll || fromZone == Zone.HAND) { + if (fromZone.match(Zone.HAND)) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) { Player player = game.getPlayer(playerInRangeId); if (player != null && !player.getHand().isEmpty()) { @@ -4383,7 +4383,7 @@ public abstract class PlayerImpl implements Player, Serializable { List activatedAll = new ArrayList<>(); // activated abilities from battlefield objects - if (fromAll || fromZone == Zone.BATTLEFIELD) { + if (fromZone.match(Zone.BATTLEFIELD)) { for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { boolean canUseActivated = permanent.canUseActivatedAbilities(game); List currentPlayable = new ArrayList<>(); @@ -4398,7 +4398,7 @@ public abstract class PlayerImpl implements Player, Serializable { } // activated abilities from stack objects - if (fromAll || fromZone == Zone.STACK) { + if (fromZone.match(Zone.STACK)) { for (StackObject stackObject : game.getState().getStack()) { List currentPlayable = new ArrayList<>(); getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); @@ -4410,7 +4410,7 @@ public abstract class PlayerImpl implements Player, Serializable { } // activated abilities from objects in the command zone (emblems or commanders) - if (fromAll || fromZone == Zone.COMMAND) { + if (fromZone.match(Zone.COMMAND)) { for (CommandObject commandObject : game.getState().getCommand()) { List currentPlayable = new ArrayList<>(); getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); From 5a63b1df2230e62e7c473e6f35f9c35a5f6699e9 Mon Sep 17 00:00:00 2001 From: theelk801 Date: Thu, 18 Dec 2025 11:12:21 -0500 Subject: [PATCH 4/4] fix verify failure --- Mage.Sets/src/mage/cards/t/TheLegendOfKuruk.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mage.Sets/src/mage/cards/t/TheLegendOfKuruk.java b/Mage.Sets/src/mage/cards/t/TheLegendOfKuruk.java index 55fb361f9f3..fdbd9c70e50 100644 --- a/Mage.Sets/src/mage/cards/t/TheLegendOfKuruk.java +++ b/Mage.Sets/src/mage/cards/t/TheLegendOfKuruk.java @@ -30,7 +30,7 @@ public final class TheLegendOfKuruk extends TransformingDoubleFacedCard { super(ownerId, setInfo, new SuperType[]{}, new CardType[]{CardType.ENCHANTMENT}, new SubType[]{SubType.SAGA}, "{2}{U}{U}", "Avatar Kuruk", - new SuperType[]{SuperType.LEGENDARY}, new CardType[]{CardType.CREATURE}, new SubType[]{SubType.AVATAR}, "U" + new SuperType[]{SuperType.LEGENDARY}, new CardType[]{CardType.CREATURE}, new SubType[]{SubType.AVATAR}, "" ); // The Legend of Kuruk