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.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}")
).setRuleAtTheTop(true));
// Flying
this.addAbility(FlyingAbility.getInstance());

View file

@ -1,13 +1,16 @@
package mage.cards.c;
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.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;
@ -30,7 +33,10 @@ 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());
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)

View file

@ -1,10 +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;
@ -33,7 +35,10 @@ 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());
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());

View file

@ -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<Cost> newCosts = new CostsImpl<>();
newCosts.add(new WaterbendCost(card.getManaValue()));
newCosts.addAll(card.getSpellAbility().getCosts());
player.setCastSourceIdWithAlternateMana(card.getId(), null, newCosts);
ManaCosts<ManaCost> manaCosts = new ManaCostsImpl<>("{0}");
manaCosts.add(new WaterbendCost(card.getManaValue()));
player.setCastSourceIdWithAlternateMana(card.getId(), manaCosts, card.getSpellAbility().getCosts());
return true;
}
}

View file

@ -5,8 +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.mana.VariableManaCost;
import mage.abilities.costs.common.WaterbendXCost;
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 WaterbendCost("{X}"), 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);
}

View file

@ -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

View file

@ -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}")
).setRuleAtTheTop(true));
// Return up to two target creatures to their owners' hands. Draw two cards.
this.getSpellAbility().addEffect(new ReturnToHandTargetEffect());

View file

@ -1,11 +1,14 @@
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.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;
@ -22,7 +25,10 @@ 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());
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()

View file

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

View file

@ -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<String> unfinished = Arrays.asList("Katara, Seeking Revenge", "Ruthless Waterbender", "Waterbender's Restoration", "Water Whip");
private static final AvatarTheLastAirbenderEternal instance = new AvatarTheLastAirbenderEternal();
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", 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()));
}
}

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.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.common.WaterbendXCost;
import mage.abilities.costs.mana.*;
import mage.abilities.effects.ContinuousEffect;
import mage.abilities.effects.Effect;
import mage.abilities.effects.Effects;
@ -25,6 +24,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 +39,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 +52,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 +352,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 +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
* Must be called after original Phyrexian mana processing and after cost modifications, e.g. on payment
@ -779,8 +815,12 @@ public abstract class AbilityImpl implements Ability {
}
}
}
if (variableManaCost != null) {
if (!variableManaCost.isPaid()) { // should only happen for human players
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()) {
@ -794,10 +834,11 @@ public abstract class AbilityImpl implements Ability {
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 = null;
String manaSymbol;
if (variableManaCost.getFilter().isBlack()) {
if (variableManaCost.getFilter().isRed()) {
manaSymbol = "B/R";
@ -812,8 +853,7 @@ public abstract class AbilityImpl implements Ability {
manaSymbol = "G";
} else if (variableManaCost.getFilter().isWhite()) {
manaSymbol = "W";
}
if (manaSymbol == null) {
} else {
throw new UnsupportedOperationException("ManaFilter is not supported: " + this);
}
for (int i = 0; i < amountMana; i++) {
@ -821,12 +861,13 @@ public abstract class AbilityImpl implements Ability {
}
}
addManaCostsToPay(new ManaCostsImpl<>(manaString.toString()));
} else {
addManaCostsToPay(new WaterbendCost(amountMana));
}
getManaCostsToPay().setX(xValue, amountMana);
setCostsTag("X", xValue);
}
variableManaCost.setPaid();
}
}
return variableManaCost;
}
@ -1701,15 +1742,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

View file

@ -1,26 +1,31 @@
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.Mana;
import mage.abilities.costs.mana.GenericManaCost;
/**
* 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
*/
public class WaterbendCost extends CostImpl {
public class WaterbendCost extends GenericManaCost {
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) {
@ -33,12 +38,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();
}
}

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.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 <T>
@ -162,6 +169,7 @@ public class ManaCostsImpl<T extends ManaCost> extends ArrayList<T> 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<T extends ManaCost> extends ArrayList<T> 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<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
public ManaCosts<T> getUnpaid() {
ManaCosts<T> unpaid = new ManaCostsImpl<>();

View file

@ -478,6 +478,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 {

View file

@ -3711,7 +3711,9 @@ public abstract class PlayerImpl implements Player, Serializable {
* @return
*/
protected boolean canPlay(ActivatedAbility ability, ManaOptions availableMana, MageObject sourceObject, Game game) {
if (!ability.isManaActivatedAbility()) {
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;
@ -3767,7 +3769,6 @@ public abstract class PlayerImpl implements Player, Serializable {
if (AbilityType.SPELL.equals(ability.getAbilityType())) {
return canPlayCardByAlternateCost(game.getCard(ability.getSourceId()), availableMana, copy, game);
}
}
return false;
}
@ -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<ActivatedAbility> 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<ActivatedAbility> 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<ActivatedAbility> 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<ActivatedAbility> currentPlayable = new ArrayList<>();
getPlayableFromObjectAll(game, Zone.COMMAND, commandObject, availableMana, currentPlayable);