implement waterbending mechanic

This commit is contained in:
theelk801 2025-12-03 18:36:03 -05:00
parent 6906072ec4
commit f007ebb289
14 changed files with 170 additions and 55 deletions

View file

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

View file

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

View file

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

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

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