This commit is contained in:
Evan Kranzler 2025-12-19 16:46:45 +01:00 committed by GitHub
commit c744d71ce9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 423 additions and 171 deletions

View file

@ -2,8 +2,10 @@ package mage.cards.b;
import mage.MageInt; import mage.MageInt;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.WaterbendCost; import mage.abilities.costs.common.WaterbendCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.keyword.ScryEffect; import mage.abilities.effects.keyword.ScryEffect;
import mage.abilities.keyword.FlyingAbility; import mage.abilities.keyword.FlyingAbility;
import mage.abilities.keyword.WardAbility; import mage.abilities.keyword.WardAbility;
@ -11,6 +13,7 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone;
import java.util.UUID; import java.util.UUID;
@ -28,6 +31,9 @@ public final class BenevolentRiverSpirit extends CardImpl {
// As an additional cost to cast this spell, waterbend {5}. // As an additional cost to cast this spell, waterbend {5}.
this.getSpellAbility().addCost(new WaterbendCost(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}")
).setRuleAtTheTop(true));
// Flying // Flying
this.addAbility(FlyingAbility.getInstance()); this.addAbility(FlyingAbility.getInstance());

View file

@ -1,13 +1,16 @@
package mage.cards.c; package mage.cards.c;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.costs.common.WaterbendCost; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.WaterbendXCost;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.TapTargetEffect; import mage.abilities.effects.common.TapTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.constants.Zone;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.filter.FilterPermanent; import mage.filter.FilterPermanent;
import mage.filter.common.FilterOpponentsCreaturePermanent; import mage.filter.common.FilterOpponentsCreaturePermanent;
@ -30,7 +33,10 @@ public final class CrashingWave extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{U}{U}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{U}{U}");
// As an additional cost to cast this spell, waterbend {X}. // As an additional cost to cast this spell, waterbend {X}.
this.getSpellAbility().addCost(new WaterbendCost("{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. // 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")); 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); TargetPermanentAmount target = new TargetPermanentAmount(3, 1, filter);
target.withNotTarget(true); target.withNotTarget(true);
player.chooseTarget(outcome, target, source, game); target.chooseTarget(outcome, player.getId(), source, game);
for (UUID targetId : target.getTargets()) { for (UUID targetId : target.getTargets()) {
Optional.ofNullable(targetId) Optional.ofNullable(targetId)
.map(game::getPermanent) .map(game::getPermanent)

View file

@ -1,10 +1,12 @@
package mage.cards.f; package mage.cards.f;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.common.delayed.AtTheBeginOfNextEndStepDelayedTriggeredAbility; 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.OneShotEffect;
import mage.abilities.effects.common.CreateTokenCopyTargetEffect; import mage.abilities.effects.common.CreateTokenCopyTargetEffect;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.SacrificeTargetEffect; import mage.abilities.effects.common.SacrificeTargetEffect;
import mage.cards.*; import mage.cards.*;
import mage.constants.CardType; import mage.constants.CardType;
@ -33,7 +35,10 @@ public final class FoggySwampVisions extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}{B}"); super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{B}{B}");
// As an additional cost to cast this spell, waterbend {X}. // As an additional cost to cast this spell, waterbend {X}.
this.getSpellAbility().addCost(new WaterbendCost("{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. // 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()); this.getSpellAbility().addEffect(new FoggySwampVisionsEffect());

View file

@ -3,10 +3,10 @@ package mage.cards.h;
import mage.MageInt; import mage.MageInt;
import mage.abilities.Ability; import mage.abilities.Ability;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; 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.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.AsThoughEffectImpl;
import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.MillCardsTargetEffect; import mage.abilities.effects.common.MillCardsTargetEffect;
@ -20,6 +20,7 @@ import mage.game.Game;
import mage.players.Player; import mage.players.Player;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.target.common.TargetCardInGraveyard; import mage.target.common.TargetCardInGraveyard;
import mage.target.common.TargetOpponent;
import mage.target.targetpointer.FixedTarget; import mage.target.targetpointer.FixedTarget;
import mage.util.CardUtil; 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. // 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 ability = new EntersBattlefieldTriggeredAbility(new MillCardsTargetEffect(3));
ability.addEffect(new HamaTheBloodbenderExileEffect()); ability.addEffect(new HamaTheBloodbenderExileEffect());
ability.addTarget(new TargetOpponent());
this.addAbility(ability); this.addAbility(ability);
} }
@ -143,10 +145,9 @@ class HamaTheBloodbenderCastEffect extends AsThoughEffectImpl {
if (player == null) { if (player == null) {
return false; return false;
} }
Costs<Cost> newCosts = new CostsImpl<>(); ManaCosts<ManaCost> manaCosts = new ManaCostsImpl<>("{0}");
newCosts.add(new WaterbendCost(card.getManaValue())); manaCosts.add(new WaterbendCost(card.getManaValue()));
newCosts.addAll(card.getSpellAbility().getCosts()); player.setCastSourceIdWithAlternateMana(card.getId(), manaCosts, card.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(card.getId(), null, newCosts);
return true; return true;
} }
} }

View file

@ -5,8 +5,7 @@ import mage.abilities.Ability;
import mage.abilities.common.ActivateIfConditionActivatedAbility; import mage.abilities.common.ActivateIfConditionActivatedAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility; import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.condition.common.MyTurnCondition; 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.dynamicvalue.common.GetXValue;
import mage.abilities.effects.common.CreateTokenEffect; import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.InfoEffect; import mage.abilities.effects.common.InfoEffect;
@ -20,7 +19,6 @@ import mage.constants.SubType;
import mage.constants.SuperType; import mage.constants.SuperType;
import mage.filter.StaticFilters; import mage.filter.StaticFilters;
import mage.game.permanent.token.AllyToken; import mage.game.permanent.token.AllyToken;
import mage.util.CardUtil;
import java.util.UUID; import java.util.UUID;
@ -49,9 +47,8 @@ public final class KataraWaterTribesHope extends CardImpl {
Ability ability = new ActivateIfConditionActivatedAbility(new SetBasePowerToughnessAllEffect( Ability ability = new ActivateIfConditionActivatedAbility(new SetBasePowerToughnessAllEffect(
GetXValue.instance, GetXValue.instance, Duration.EndOfTurn, GetXValue.instance, GetXValue.instance, Duration.EndOfTurn,
StaticFilters.FILTER_CONTROLLED_CREATURES StaticFilters.FILTER_CONTROLLED_CREATURES
), new WaterbendCost("{X}"), MyTurnCondition.instance); ), new WaterbendXCost(1), MyTurnCondition.instance);
ability.addEffect(new InfoEffect("X can't be 0")); ability.addEffect(new InfoEffect("X can't be 0"));
CardUtil.castStream(ability.getCosts(), VariableManaCost.class).forEach(cost -> cost.setMinX(1));
this.addAbility(ability); this.addAbility(ability);
} }

View file

@ -30,7 +30,7 @@ public final class TheLegendOfKuruk extends TransformingDoubleFacedCard {
super(ownerId, setInfo, super(ownerId, setInfo,
new SuperType[]{}, new CardType[]{CardType.ENCHANTMENT}, new SubType[]{SubType.SAGA}, "{2}{U}{U}", new SuperType[]{}, new CardType[]{CardType.ENCHANTMENT}, new SubType[]{SubType.SAGA}, "{2}{U}{U}",
"Avatar Kuruk", "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 // The Legend of Kuruk

View file

@ -1,12 +1,15 @@
package mage.cards.w; package mage.cards.w;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.WaterbendCost; import mage.abilities.costs.common.WaterbendCost;
import mage.abilities.effects.common.DrawCardSourceControllerEffect; import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.effects.common.ReturnToHandTargetEffect; import mage.abilities.effects.common.ReturnToHandTargetEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone;
import mage.target.common.TargetCreaturePermanent; import mage.target.common.TargetCreaturePermanent;
import java.util.UUID; import java.util.UUID;
@ -23,6 +26,9 @@ public final class WaterWhip extends CardImpl {
// As an additional cost to cast this spell, waterbend {5}. // As an additional cost to cast this spell, waterbend {5}.
this.getSpellAbility().addCost(new WaterbendCost(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}")
).setRuleAtTheTop(true));
// Return up to two target creatures to their owners' hands. Draw two cards. // Return up to two target creatures to their owners' hands. Draw two cards.
this.getSpellAbility().addEffect(new ReturnToHandTargetEffect()); this.getSpellAbility().addEffect(new ReturnToHandTargetEffect());

View file

@ -1,11 +1,14 @@
package mage.cards.w; package mage.cards.w;
import mage.abilities.costs.common.WaterbendCost; import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.common.WaterbendXCost;
import mage.abilities.effects.common.ExileReturnBattlefieldNextEndStepTargetEffect; import mage.abilities.effects.common.ExileReturnBattlefieldNextEndStepTargetEffect;
import mage.abilities.effects.common.InfoEffect;
import mage.cards.CardImpl; import mage.cards.CardImpl;
import mage.cards.CardSetInfo; import mage.cards.CardSetInfo;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.SubType; import mage.constants.SubType;
import mage.constants.Zone;
import mage.target.common.TargetControlledCreaturePermanent; import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.targetadjustment.XTargetsCountAdjuster; import mage.target.targetadjustment.XTargetsCountAdjuster;
@ -22,7 +25,10 @@ public final class WaterbendersRestoration extends CardImpl {
this.subtype.add(SubType.LESSON); this.subtype.add(SubType.LESSON);
// As an additional cost to cast this spell, waterbend {X}. // As an additional cost to cast this spell, waterbend {X}.
this.getSpellAbility().addCost(new WaterbendCost("{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. // 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() this.getSpellAbility().addEffect(new ExileReturnBattlefieldNextEndStepTargetEffect()

View file

@ -4,15 +4,11 @@ import mage.cards.ExpansionSet;
import mage.constants.Rarity; import mage.constants.Rarity;
import mage.constants.SetType; import mage.constants.SetType;
import java.util.Arrays;
import java.util.List;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
public final class AvatarTheLastAirbender extends ExpansionSet { public final class AvatarTheLastAirbender extends ExpansionSet {
private static final List<String> 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(); private static final AvatarTheLastAirbender instance = new AvatarTheLastAirbender();
public static AvatarTheLastAirbender getInstance() { public static AvatarTheLastAirbender getInstance() {
@ -420,7 +416,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", 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, 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.add(new SetCardInfo("Zuko, Exiled Prince", 163, Rarity.UNCOMMON, mage.cards.z.ZukoExiledPrince.class));
cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName()));
} }
} }

View file

@ -4,16 +4,11 @@ import mage.cards.ExpansionSet;
import mage.constants.Rarity; import mage.constants.Rarity;
import mage.constants.SetType; import mage.constants.SetType;
import java.util.Arrays;
import java.util.List;
/** /**
* @author TheElk801 * @author TheElk801
*/ */
public final class AvatarTheLastAirbenderEternal extends ExpansionSet { public final class AvatarTheLastAirbenderEternal extends ExpansionSet {
private static final List<String> unfinished = Arrays.asList("Katara, Seeking Revenge", "Ruthless Waterbender", "Waterbender's Restoration", "Water Whip");
private static final AvatarTheLastAirbenderEternal instance = new AvatarTheLastAirbenderEternal(); private static final AvatarTheLastAirbenderEternal instance = new AvatarTheLastAirbenderEternal();
public static AvatarTheLastAirbenderEternal getInstance() { public static AvatarTheLastAirbenderEternal getInstance() {
@ -338,7 +333,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", 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, 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.add(new SetCardInfo("Zuko, Seeking Honor", 150, Rarity.UNCOMMON, mage.cards.z.ZukoSeekingHonor.class));
cards.removeIf(setCardInfo -> unfinished.contains(setCardInfo.getName()));
} }
} }

View file

@ -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);
}
}

View file

@ -7,10 +7,9 @@ import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.condition.Condition; import mage.abilities.condition.Condition;
import mage.abilities.costs.*; import mage.abilities.costs.*;
import mage.abilities.costs.common.PayLifeCost; import mage.abilities.costs.common.PayLifeCost;
import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.common.WaterbendCost;
import mage.abilities.costs.mana.ManaCosts; import mage.abilities.costs.common.WaterbendXCost;
import mage.abilities.costs.mana.ManaCostsImpl; import mage.abilities.costs.mana.*;
import mage.abilities.costs.mana.VariableManaCost;
import mage.abilities.effects.ContinuousEffect; import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.Effect; import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects; import mage.abilities.effects.Effects;
@ -25,6 +24,7 @@ import mage.choices.ChoiceHintType;
import mage.choices.ChoiceImpl; import mage.choices.ChoiceImpl;
import mage.constants.*; import mage.constants.*;
import mage.filter.FilterMana; import mage.filter.FilterMana;
import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.command.Dungeon; import mage.game.command.Dungeon;
import mage.game.command.Emblem; import mage.game.command.Emblem;
@ -39,8 +39,10 @@ import mage.game.stack.StackAbility;
import mage.players.Player; import mage.players.Player;
import mage.target.Target; import mage.target.Target;
import mage.target.TargetCard; import mage.target.TargetCard;
import mage.target.TargetPermanent;
import mage.target.Targets; import mage.target.Targets;
import mage.target.common.TargetCardInLibrary; import mage.target.common.TargetCardInLibrary;
import mage.target.common.TargetControlledPermanent;
import mage.target.targetadjustment.GenericTargetAdjuster; import mage.target.targetadjustment.GenericTargetAdjuster;
import mage.target.targetadjustment.TargetAdjuster; import mage.target.targetadjustment.TargetAdjuster;
import mage.util.CardUtil; import mage.util.CardUtil;
@ -50,6 +52,7 @@ import mage.watchers.Watcher;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -349,6 +352,7 @@ public abstract class AbilityImpl implements Ability {
// Phyrexian mana symbols, the player announces whether they intend to pay 2 // Phyrexian mana symbols, the player announces whether they intend to pay 2
// life or the corresponding colored mana cost for each of those symbols. // life or the corresponding colored mana cost for each of those symbols.
AbilityImpl.handlePhyrexianCosts(game, this, this, this.getManaCostsToPay()); AbilityImpl.handlePhyrexianCosts(game, this, this, this.getManaCostsToPay());
AbilityImpl.handleWaterbendingCosts(game, this, this, this.getManaCostsToPay());
// 20241022 - 601.2b // 20241022 - 601.2b
// Not yet included in 601.2b but this is where it will be // Not yet included in 601.2b but this is where it will be
@ -687,6 +691,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<Permanent> 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 * 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 * Must be called after original Phyrexian mana processing and after cost modifications, e.g. on payment
@ -779,54 +815,59 @@ public abstract class AbilityImpl implements Ability {
} }
} }
} }
if (variableManaCost != null) { if (variableManaCost == null) {
if (!variableManaCost.isPaid()) { // should only happen for human players return variableManaCost;
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.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; return variableManaCost;
} }
@ -1701,15 +1742,15 @@ public abstract class AbilityImpl implements Ability {
@Override @Override
public void initSourceObjectZoneChangeCounter(Game game, boolean force) { public void initSourceObjectZoneChangeCounter(Game game, boolean force) {
if (!(this instanceof MageSingleton) && (force || sourceObjectZoneChangeCounter == 0 )) { if (!(this instanceof MageSingleton) && (force || sourceObjectZoneChangeCounter == 0)) {
setSourceObjectZoneChangeCounter(getCurrentSourceObjectZoneChangeCounter(game)); setSourceObjectZoneChangeCounter(getCurrentSourceObjectZoneChangeCounter(game));
} }
} }
private int getCurrentSourceObjectZoneChangeCounter(Game game){ private int getCurrentSourceObjectZoneChangeCounter(Game game) {
int zcc = game.getState().getZoneChangeCounter(getSourceId()); int zcc = game.getState().getZoneChangeCounter(getSourceId());
Permanent p = game.getPermanentEntering(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 // 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 // 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 // So "Enters with counters" causes "Whenever counters are placed" to trigger with battlefield zcc

View file

@ -1,26 +1,31 @@
package mage.abilities.costs.common; package mage.abilities.costs.common;
import mage.abilities.Ability; import mage.Mana;
import mage.abilities.costs.Cost; import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.costs.CostImpl;
import mage.game.Game;
import java.util.UUID;
/** /**
* TODO: Implement properly * 701.67. Waterbend
* <p>
* 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.
* <p>
* 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.
* <p>
* 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 * @author TheElk801
*/ */
public class WaterbendCost extends CostImpl { public class WaterbendCost extends GenericManaCost {
public WaterbendCost(int amount) { public WaterbendCost(int amount) {
this("{" + amount + '}'); super(amount);
} for (int i = 0; i < amount; i++) {
options.add(Mana.ColorlessMana(i));
public WaterbendCost(String mana) { }
super();
this.text = "waterbend " + mana;
} }
private WaterbendCost(final WaterbendCost cost) { private WaterbendCost(final WaterbendCost cost) {
@ -33,12 +38,7 @@ public class WaterbendCost extends CostImpl {
} }
@Override @Override
public boolean canPay(Ability ability, Ability source, UUID controllerId, Game game) { public String getText() {
return false; return "waterbend " + super.getText();
}
@Override
public boolean pay(Ability ability, Game game, Ability source, UUID controllerId, boolean noMana, Cost costToPay) {
return false;
} }
} }

View file

@ -0,0 +1,36 @@
package mage.abilities.costs.common;
import mage.abilities.costs.VariableCostType;
import mage.abilities.costs.mana.VariableManaCost;
/**
* 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 VariableManaCost {
public WaterbendXCost() {
this(0);
}
public WaterbendXCost(int minX) {
super(VariableCostType.NORMAL);
this.setMinX(minX);
}
private WaterbendXCost(final WaterbendXCost cost) {
super(cost);
}
@Override
public WaterbendXCost copy() {
return new WaterbendXCost(this);
}
@Override
public String getText() {
return "waterbend {X}";
}
}

View file

@ -5,20 +5,27 @@ import mage.abilities.Ability;
import mage.abilities.AbilityImpl; import mage.abilities.AbilityImpl;
import mage.abilities.costs.*; import mage.abilities.costs.*;
import mage.abilities.costs.common.PayLifeCost; import mage.abilities.costs.common.PayLifeCost;
import mage.abilities.costs.common.WaterbendCost;
import mage.abilities.mana.ManaOptions; import mage.abilities.mana.ManaOptions;
import mage.constants.ColoredManaSymbol; import mage.constants.ColoredManaSymbol;
import mage.constants.ManaType; import mage.constants.ManaType;
import mage.constants.Outcome; import mage.constants.Outcome;
import mage.filter.Filter; import mage.filter.Filter;
import mage.filter.StaticFilters;
import mage.game.Game; import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.ManaPool; import mage.players.ManaPool;
import mage.players.Player; import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.Targets; import mage.target.Targets;
import mage.target.common.TargetControlledPermanent;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.ManaUtil; import mage.util.ManaUtil;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/** /**
* @param <T> * @param <T>
@ -162,6 +169,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
if (payingPlayer != null) { if (payingPlayer != null) {
int bookmark = game.bookmarkState(); int bookmark = game.bookmarkState();
handlePhyrexianManaCosts(ability, payingPlayer, source, game); handlePhyrexianManaCosts(ability, payingPlayer, source, game);
handleWaterbendingCosts(ability, payingPlayer, source, game);
if (pay(ability, game, source, payingPlayerId, false, null)) { if (pay(ability, game, source, payingPlayerId, false, null)) {
game.removeBookmark(bookmark); game.removeBookmark(bookmark);
return true; return true;
@ -195,6 +203,33 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> implements M
tempCosts.pay(source, game, source, payingPlayer.getId(), false, null); 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<Permanent> 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 @Override
public ManaCosts<T> getUnpaid() { public ManaCosts<T> getUnpaid() {
ManaCosts<T> unpaid = new ManaCostsImpl<>(); ManaCosts<T> unpaid = new ManaCostsImpl<>();

View file

@ -478,6 +478,17 @@ public final class StaticFilters {
FILTER_CONTROLLED_PERMANENT_ARTIFACT_OR_CREATURE.setLockedFilter(true); 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"); public static final FilterControlledPermanent FILTER_CONTROLLED_ARTIFACT_OR_OTHER_CREATURE = new FilterControlledPermanent("another creature or an artifact");
static { static {

View file

@ -3711,62 +3711,63 @@ public abstract class PlayerImpl implements Player, Serializable {
* @return * @return
*/ */
protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) { protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) {
if (!ability.isManaActivatedAbility()) { if (ability.isManaActivatedAbility()) {
ActivatedAbility copy = ability.copy(); // Copy is needed because cost reduction effects modify e.g. the mana to activate/cast the ability return false;
if (!copy.canActivate(playerId, game).canActivate()) { }
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<MageIdentifier> 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<Cost> 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 if (canPutToPlay) {
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<MageIdentifier> 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; return true;
} }
}
// ALTERNATIVE COST FROM dynamic effects // ALTERNATIVE COST from source card (any AlternativeSourceCosts)
for (MageIdentifier identifier : getCastSourceIdWithAlternateMana().getOrDefault(copy.getSourceId(), new HashSet<>())) { if (AbilityType.SPELL.equals(ability.getAbilityType())) {
if (allowedIdentifiers != null && !(allowedIdentifiers.contains(MageIdentifier.Default) || allowedIdentifiers.contains(identifier))) { return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game);
continue;
}
ManaCosts alternateCosts = getCastSourceIdManaCosts().get(copy.getSourceId()).get(identifier);
Costs<Cost> 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);
}
} }
return false; return false;
} }
@ -4110,7 +4111,7 @@ public abstract class PlayerImpl implements Player, Serializable {
TransformingDoubleFacedCard mainCard = (TransformingDoubleFacedCard) object; TransformingDoubleFacedCard mainCard = (TransformingDoubleFacedCard) object;
getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output); getPlayableFromObjectSingle(game, fromZone, mainCard.getLeftHalfCard(), mainCard.getLeftHalfCard().getAbilities(game), availableMana, output);
getPlayableFromObjectSingle(game, fromZone, mainCard, mainCard.getSharedAbilities(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) // adventure must use different card characteristics for different spells (main or adventure)
CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object; CardWithSpellOption cardWithSpellOption = (CardWithSpellOption) object;
getPlayableFromObjectSingle(game, fromZone, cardWithSpellOption.getSpellCard(), cardWithSpellOption.getSpellCard().getAbilities(game), availableMana, output); 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(); Game game = originalGame.createSimulationForPlayableCalc();
ManaOptions availableMana = getManaAvailable(game); // get available mana options (mana pool and conditional mana added (but conditional still lose condition)) 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 && fromZone.match(Zone.HAND)) {
if (hidden && (fromAll || fromZone == Zone.HAND)) {
for (Card card : hand.getCards(game)) { for (Card card : hand.getCards(game)) {
for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?) for (Ability ability : card.getAbilities(game)) { // gets this activated ability from hand? (Morph?)
if (ability.getZone().match(Zone.HAND)) { 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)) { for (UUID playerId : game.getState().getPlayersInRange(getId(), game)) {
Player player = game.getPlayer(playerId); Player player = game.getPlayer(playerId);
if (player == null) { 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 (ExileZone exile : game.getExile().getExileZones()) {
for (Card card : exile.getCards(game)) { for (Card card : exile.getCards(game)) {
getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable); getPlayableFromObjectAll(game, Zone.EXILED, card, availableMana, playable);
@ -4321,7 +4321,7 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
// check to play revealed cards // check to play revealed cards
if (fromAll) { if (fromZone.match(Zone.ALL)) {
for (Cards revealedCards : game.getState().getRevealed().values()) { for (Cards revealedCards : game.getState().getRevealed().values()) {
for (Card card : revealedCards.getCards(game)) { for (Card card : revealedCards.getCards(game)) {
// revealed cards can be from any zones // revealed cards can be from any zones
@ -4331,7 +4331,7 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
// outside cards // outside cards
if (fromAll || fromZone == Zone.OUTSIDE) { if (fromZone.match(Zone.OUTSIDE)) {
// companion cards // companion cards
for (Cards companionCards : game.getState().getCompanion().values()) { for (Cards companionCards : game.getState().getCompanion().values()) {
for (Card card : companionCards.getCards(game)) { 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 // 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)) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {
Player player = game.getPlayer(playerInRangeId); Player player = game.getPlayer(playerInRangeId);
if (player != null && player.getLibrary().hasCards()) { 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)? // TODO: remove direct hand check (reveal fix in Sen Triplets)?
// human games: cards from opponent's hand must be revealed before play // 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 // 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)) { for (UUID playerInRangeId : game.getState().getPlayersInRange(getId(), game)) {
Player player = game.getPlayer(playerInRangeId); Player player = game.getPlayer(playerInRangeId);
if (player != null && !player.getHand().isEmpty()) { if (player != null && !player.getHand().isEmpty()) {
@ -4383,7 +4383,7 @@ public abstract class PlayerImpl implements Player, Serializable {
List<ActivatedAbility> activatedAll = new ArrayList<>(); List<ActivatedAbility> activatedAll = new ArrayList<>();
// activated abilities from battlefield objects // activated abilities from battlefield objects
if (fromAll || fromZone == Zone.BATTLEFIELD) { if (fromZone.match(Zone.BATTLEFIELD)) {
for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) { for (Permanent permanent : game.getBattlefield().getAllActivePermanents()) {
boolean canUseActivated = permanent.canUseActivatedAbilities(game); boolean canUseActivated = permanent.canUseActivatedAbilities(game);
List<ActivatedAbility> currentPlayable = new ArrayList<>(); List<ActivatedAbility> currentPlayable = new ArrayList<>();
@ -4398,7 +4398,7 @@ public abstract class PlayerImpl implements Player, Serializable {
} }
// activated abilities from stack objects // activated abilities from stack objects
if (fromAll || fromZone == Zone.STACK) { if (fromZone.match(Zone.STACK)) {
for (StackObject stackObject : game.getState().getStack()) { for (StackObject stackObject : game.getState().getStack()) {
List<ActivatedAbility> currentPlayable = new ArrayList<>(); List<ActivatedAbility> currentPlayable = new ArrayList<>();
getPlayableFromObjectAll(game, Zone.STACK, stackObject, availableMana, currentPlayable); 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) // 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()) { for (CommandObject commandObject : game.getState().getCommand()) {
List<ActivatedAbility> currentPlayable = new ArrayList<>(); List<ActivatedAbility> currentPlayable = new ArrayList<>();
getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable); getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable);