Initial checkin for Waterbending (#9120)

This commit is contained in:
Chris H
2025-11-10 05:27:44 -05:00
committed by GitHub
parent 219e3d6182
commit c9a5fe9135
18 changed files with 186 additions and 47 deletions

View File

@@ -1345,9 +1345,7 @@ public class ComputerUtilMana {
} }
} }
if (!effect) { CostAdjustment.adjust(manaCost, sa, null, test, effect);
CostAdjustment.adjust(manaCost, sa, null, test);
}
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
ManaCost mkCost = sa.getPayCosts().getTotalMana(); ManaCost mkCost = sa.getPayCosts().getTotalMana();
@@ -1773,15 +1771,18 @@ public class ComputerUtilMana {
/** /**
* Matches list of creatures to shards in mana cost for convoking. * Matches list of creatures to shards in mana cost for convoking.
* @param cost cost of convoked ability *
* @param list creatures to be evaluated * @param cost cost of convoked ability
* @param improvise * @param list creatures to be evaluated
* @param artifacts
* @param creatures
* @return map between creatures and shards to convoke * @return map between creatures and shards to convoke
*/ */
public static Map<Card, ManaCostShard> getConvokeOrImproviseFromList(final ManaCost cost, List<Card> list, boolean improvise) { public static Map<Card, ManaCostShard> getConvokeOrImproviseFromList(final ManaCost cost, List<Card> list, boolean artifacts, boolean creatures) {
final Map<Card, ManaCostShard> convoke = new HashMap<>(); final Map<Card, ManaCostShard> convoke = new HashMap<>();
Card convoked = null; Card convoked = null;
if (!improvise) { if (creatures && !artifacts) {
// Run for convoke but not improvise or waterbending
for (ManaCostShard toPay : cost) { for (ManaCostShard toPay : cost) {
if (toPay.isSnow() || toPay.isColorless()) { if (toPay.isSnow() || toPay.isColorless()) {
continue; continue;

View File

@@ -1382,7 +1382,7 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean improvise) { public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean artifacts, boolean creatures, Integer maxReduction) {
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler(); final PhaseHandler ph = ai.getGame().getPhaseHandler();
//Filter out mana sources that will interfere with payManaCost() //Filter out mana sources that will interfere with payManaCost()
@@ -1390,9 +1390,10 @@ public class PlayerControllerAi extends PlayerController {
// Filter out creatures if AI hasn't attacked yet // Filter out creatures if AI hasn't attacked yet
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
if (improvise) { if (!creatures) {
untapped = CardLists.filter(untapped, c -> !c.isCreature()); untapped = CardLists.filter(untapped, c -> !c.isCreature());
} else { } else {
// TODO AI needs to learn how to use Convoke or Waterbend
return new HashMap<>(); return new HashMap<>();
} }
} }
@@ -1406,13 +1407,16 @@ public class PlayerControllerAi extends PlayerController {
if (!ai.getGame().getStack().isEmpty()) { if (!ai.getGame().getStack().isEmpty()) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), null); final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), null);
for (Card c : blockers) { for (Card c : blockers) {
if (objects.contains(c) && (!improvise || c.isArtifact())) { if (objects.contains(c) && (creatures || c.isArtifact())) {
untapped.add(c); untapped.add(c);
} }
if (maxReduction != null && untapped.size() >= maxReduction) {
break;
}
} }
} }
} }
return ComputerUtilMana.getConvokeOrImproviseFromList(manaCost, untapped, improvise); return ComputerUtilMana.getConvokeOrImproviseFromList(manaCost, untapped, artifacts, creatures);
} }
@Override @Override

View File

@@ -15,6 +15,7 @@ import forge.game.*;
import forge.game.ability.AbilityFactory.AbilityRecordType; import forge.game.ability.AbilityFactory.AbilityRecordType;
import forge.game.card.*; import forge.game.card.*;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostAdjustment;
import forge.game.cost.IndividualCostPaymentInstance; import forge.game.cost.IndividualCostPaymentInstance;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface; import forge.game.keyword.KeywordInterface;
@@ -1527,6 +1528,7 @@ public class AbilityUtils {
else { else {
cost = new Cost(unlessCost, true); cost = new Cost(unlessCost, true);
} }
cost = CostAdjustment.adjust(cost, sa, true);
return cost; return cost;
} }

View File

@@ -188,6 +188,15 @@ public class Cost implements Serializable {
return this.isAbility; return this.isAbility;
} }
public final String getMaxWaterbend() {
for (CostPart cp : this.costParts) {
if (cp instanceof CostPartMana) {
return ((CostPartMana) cp).getMaxWaterbend();
}
}
return null;
}
private Cost() { private Cost() {
} }
@@ -564,6 +573,11 @@ public class Cost implements Serializable {
return new CostRevealChosen(splitStr[0], splitStr.length > 1 ? splitStr[1] : null); return new CostRevealChosen(splitStr[0], splitStr.length > 1 ? splitStr[1] : null);
} }
if (parse.startsWith("Waterbend<")) {
final String[] splitStr = abCostParse(parse, 1);
return new CostWaterbend(splitStr[0]);
}
if (parse.equals("Forage")) { if (parse.equals("Forage")) {
return new CostForage(); return new CostForage();
} }
@@ -973,6 +987,7 @@ public class Cost implements Serializable {
} else { } else {
costParts.add(0, new CostPartMana(manaCost.toManaCost(), null)); costParts.add(0, new CostPartMana(manaCost.toManaCost(), null));
} }
getCostMana().setMaxWaterbend(mPart.getMaxWaterbend());
} else if (part instanceof CostPutCounter || (mergeAdditional && // below usually not desired because they're from different causes } else if (part instanceof CostPutCounter || (mergeAdditional && // below usually not desired because they're from different causes
(part instanceof CostDiscard || part instanceof CostDraw || (part instanceof CostDiscard || part instanceof CostDraw ||
part instanceof CostAddMana || part instanceof CostPayLife || part instanceof CostAddMana || part instanceof CostPayLife ||

View File

@@ -31,11 +31,13 @@ import org.apache.commons.lang3.StringUtils;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.function.Predicate;
public class CostAdjustment { public class CostAdjustment {
public static Cost adjust(final Cost cost, final SpellAbility sa, boolean effect) { public static Cost adjust(final Cost cost, final SpellAbility sa, boolean effect) {
if (sa.isTrigger() || cost == null || effect) { if (sa.isTrigger() || cost == null || effect) {
sa.setMaxWaterbend(cost);
return cost; return cost;
} }
@@ -99,6 +101,9 @@ public class CostAdjustment {
host.setState(CardStateName.Original, false); host.setState(CardStateName.Original, false);
host.setFaceDown(false); host.setFaceDown(false);
} }
sa.setMaxWaterbend(result);
return result; return result;
} }
@@ -171,21 +176,22 @@ public class CostAdjustment {
// If cardsToDelveOut is null, will immediately exile the delved cards and remember them on the host card. // If cardsToDelveOut is null, will immediately exile the delved cards and remember them on the host card.
// Otherwise, will return them in cardsToDelveOut and the caller is responsible for doing the above. // Otherwise, will return them in cardsToDelveOut and the caller is responsible for doing the above.
public static boolean adjust(ManaCostBeingPaid cost, final SpellAbility sa, CardCollection cardsToDelveOut, boolean test) { public static boolean adjust(ManaCostBeingPaid cost, final SpellAbility sa, CardCollection cardsToDelveOut, boolean test, boolean effect) {
if (sa.isTrigger() || sa.isReplacementAbility()) { if (effect) {
adjustCostByWaterbend(cost, sa, test);
}
if (effect || sa.isTrigger() || sa.isReplacementAbility()) {
return true; return true;
} }
final Game game = sa.getActivatingPlayer().getGame(); final Game game = sa.getActivatingPlayer().getGame();
final Card originalCard = sa.getHostCard(); final Card originalCard = sa.getHostCard();
boolean isStateChangeToFaceDown = false;
if (sa.isSpell()) { boolean isStateChangeToFaceDown = false;
if (sa.isCastFaceDown() && !originalCard.isFaceDown()) { if (sa.isSpell() && sa.isCastFaceDown() && !originalCard.isFaceDown()) {
// Turn face down to apply cost modifiers correctly // Turn face down to apply cost modifiers correctly
originalCard.turnFaceDownNoUpdate(); originalCard.turnFaceDownNoUpdate();
isStateChangeToFaceDown = true; isStateChangeToFaceDown = true;
}
} }
CardCollection cardsOnBattlefield = new CardCollection(game.getCardsIn(ZoneType.Battlefield)); CardCollection cardsOnBattlefield = new CardCollection(game.getCardsIn(ZoneType.Battlefield));
@@ -278,17 +284,19 @@ public class CostAdjustment {
table.triggerChangesZoneAll(game, sa); table.triggerChangesZoneAll(game, sa);
} }
if (sa.getHostCard().hasKeyword(Keyword.CONVOKE)) { if (sa.getHostCard().hasKeyword(Keyword.CONVOKE)) {
adjustCostByConvokeOrImprovise(cost, sa, false, test); adjustCostByConvokeOrImprovise(cost, sa, false, true, test);
} }
if (sa.getHostCard().hasKeyword(Keyword.IMPROVISE)) { if (sa.getHostCard().hasKeyword(Keyword.IMPROVISE)) {
adjustCostByConvokeOrImprovise(cost, sa, true, test); adjustCostByConvokeOrImprovise(cost, sa, true, false, test);
} }
} // isSpell } // isSpell
if (sa.hasParam("TapCreaturesForMana")) { if (sa.hasParam("TapCreaturesForMana")) {
adjustCostByConvokeOrImprovise(cost, sa, false, test); adjustCostByConvokeOrImprovise(cost, sa, false, true, test);
} }
adjustCostByWaterbend(cost, sa, test);
// Reset card state (if changed) // Reset card state (if changed)
if (isStateChangeToFaceDown) { if (isStateChangeToFaceDown) {
originalCard.setFaceDown(false); originalCard.setFaceDown(false);
@@ -299,6 +307,13 @@ public class CostAdjustment {
} }
// GetSpellCostChange // GetSpellCostChange
private static void adjustCostByWaterbend(ManaCostBeingPaid cost, SpellAbility sa, boolean test) {
Integer maxWaterbend = sa.getMaxWaterbend();
if (maxWaterbend != null && maxWaterbend > 0) {
adjustCostByConvokeOrImprovise(cost, sa, true, true, test);
}
}
private static boolean adjustCostByAssist(ManaCostBeingPaid cost, final SpellAbility sa, boolean test) { private static boolean adjustCostByAssist(ManaCostBeingPaid cost, final SpellAbility sa, boolean test) {
// 702.132a Assist is a static ability that modifies the rules of paying for the spell with assist (see rules 601.2g-h). // 702.132a Assist is a static ability that modifies the rules of paying for the spell with assist (see rules 601.2g-h).
// If the total cost to cast a spell with assist includes a generic mana component, before you activate mana abilities while casting it, you may choose another player. // If the total cost to cast a spell with assist includes a generic mana component, before you activate mana abilities while casting it, you may choose another player.
@@ -321,27 +336,33 @@ public class CostAdjustment {
return assistant.getController().helpPayForAssistSpell(cost, sa, genericLeft, requestedAmount); return assistant.getController().helpPayForAssistSpell(cost, sa, genericLeft, requestedAmount);
} }
private static void adjustCostByConvokeOrImprovise(ManaCostBeingPaid cost, final SpellAbility sa, boolean improvise, boolean test) { private static void adjustCostByConvokeOrImprovise(ManaCostBeingPaid cost, final SpellAbility sa, boolean artifacts, boolean creatures, boolean test) {
if (!improvise) { if (creatures && !artifacts) {
sa.clearTappedForConvoke(); sa.clearTappedForConvoke();
} }
final Player activator = sa.getActivatingPlayer(); final Player activator = sa.getActivatingPlayer();
CardCollectionView untappedCards = CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), CardCollectionView untappedCards = CardLists.filter(activator.getCardsIn(ZoneType.Battlefield),
CardPredicates.CAN_TAP); CardPredicates.CAN_TAP);
if (improvise) {
Integer maxReduction = null;
if (artifacts && creatures) {
maxReduction = sa.getMaxWaterbend();
Predicate <Card> isArtifactOrCreature = card -> card.isArtifact() || card.isCreature();
untappedCards = CardLists.filter(untappedCards, isArtifactOrCreature);
} else if (artifacts) {
untappedCards = CardLists.filter(untappedCards, CardPredicates.ARTIFACTS); untappedCards = CardLists.filter(untappedCards, CardPredicates.ARTIFACTS);
} else { } else {
untappedCards = CardLists.filter(untappedCards, CardPredicates.CREATURES); untappedCards = CardLists.filter(untappedCards, CardPredicates.CREATURES);
} }
Map<Card, ManaCostShard> convokedCards = activator.getController().chooseCardsForConvokeOrImprovise(sa, Map<Card, ManaCostShard> convokedCards = activator.getController().chooseCardsForConvokeOrImprovise(sa,
cost.toManaCost(), untappedCards, improvise); cost.toManaCost(), untappedCards, artifacts, creatures, maxReduction);
CardCollection tapped = new CardCollection(); CardCollection tapped = new CardCollection();
for (final Entry<Card, ManaCostShard> conv : convokedCards.entrySet()) { for (final Entry<Card, ManaCostShard> conv : convokedCards.entrySet()) {
Card c = conv.getKey(); Card c = conv.getKey();
if (!improvise) { if (creatures && !artifacts) {
sa.addTappedForConvoke(c); sa.addTappedForConvoke(c);
} }
cost.decreaseShard(conv.getValue(), 1); cost.decreaseShard(conv.getValue(), 1);

View File

@@ -39,6 +39,8 @@ public class CostPartMana extends CostPart {
private boolean isEnchantedCreatureCost = false; private boolean isEnchantedCreatureCost = false;
private boolean isCostPayAnyNumberOfTimes = false; private boolean isCostPayAnyNumberOfTimes = false;
protected String maxWaterbend;
public int paymentOrder() { return shouldPayLast() ? 200 : 0; } public int paymentOrder() { return shouldPayLast() ? 200 : 0; }
public boolean shouldPayLast() { public boolean shouldPayLast() {
@@ -63,6 +65,13 @@ public class CostPartMana extends CostPart {
this.isEnchantedCreatureCost = enchantedCreatureCost; this.isEnchantedCreatureCost = enchantedCreatureCost;
} }
public String getMaxWaterbend() {
return maxWaterbend;
}
public void setMaxWaterbend(String max) {
maxWaterbend = max;
}
/** /**
* Gets the mana. * Gets the mana.
* *
@@ -101,7 +110,7 @@ public class CostPartMana extends CostPart {
public boolean isUndoable() { return true; } public boolean isUndoable() { return true; }
@Override @Override
public final String toString() { public String toString() {
return cost.toString(); return cost.toString();
} }

View File

@@ -0,0 +1,18 @@
package forge.game.cost;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
public class CostWaterbend extends CostPartMana {
public CostWaterbend(final String mana) {
super(new ManaCost(new ManaCostParser(mana)), null);
maxWaterbend = mana;
}
@Override
public final String toString() {
return "Waterbend " + getMana().toString();
}
}

View File

@@ -202,7 +202,7 @@ public abstract class PlayerController {
public abstract CardCollection chooseCardsToDiscardToMaximumHandSize(int numDiscard); public abstract CardCollection chooseCardsToDiscardToMaximumHandSize(int numDiscard);
public abstract CardCollectionView chooseCardsToDelve(int genericAmount, CardCollection grave); public abstract CardCollectionView chooseCardsToDelve(int genericAmount, CardCollection grave);
public abstract Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean improvise); public abstract Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean artifacts, boolean creatures, Integer maxReduction);
public abstract List<Card> chooseCardsForSplice(SpellAbility sa, List<Card> cards); public abstract List<Card> chooseCardsForSplice(SpellAbility sa, List<Card> cards);
public abstract CardCollectionView chooseCardsToRevealFromHand(int min, int max, CardCollectionView valid); public abstract CardCollectionView chooseCardsToRevealFromHand(int min, int max, CardCollectionView valid);

View File

@@ -144,6 +144,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private final Supplier<CardCollection> tappedForConvoke = Suppliers.memoize(CardCollection::new); private final Supplier<CardCollection> tappedForConvoke = Suppliers.memoize(CardCollection::new);
private Card sacrificedAsOffering; private Card sacrificedAsOffering;
private Card sacrificedAsEmerge; private Card sacrificedAsEmerge;
private Integer maxWaterbend;
private AbilityManaPart manaPart; private AbilityManaPart manaPart;
@@ -2692,4 +2693,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
public Integer getMaxWaterbend() {
return maxWaterbend;
}
public void setMaxWaterbend(Cost cost) {
if (cost == null || cost.getMaxWaterbend() == null) {
return;
}
maxWaterbend = AbilityUtils.calculateAmount(getHostCard(), cost.getMaxWaterbend(), this);
}
} }

View File

@@ -428,6 +428,10 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
game.getTriggerHandler().runTrigger(TriggerType.AbilityCast, runParams, true); game.getTriggerHandler().runTrigger(TriggerType.AbilityCast, runParams, true);
} }
if (sp.getPayCosts() != null && sp.getMaxWaterbend() != null) {
activator.triggerElementalBend(TriggerType.Waterbend);
}
// Run Cycled triggers // Run Cycled triggers
if (sp.isCycling()) { if (sp.isCycling()) {
activator.addCycled(sp); activator.addCycled(sp);

View File

@@ -690,7 +690,7 @@ public class PlayerControllerForTests extends PlayerController {
@Override @Override
public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost,
CardCollectionView untappedCards, boolean improvise) { CardCollectionView untappedCards, boolean artifacts, boolean creatures, Integer maxReduction) {
// TODO: AI to choose a creature to tap would go here // TODO: AI to choose a creature to tap would go here
// Probably along with deciding how many creatures to tap // Probably along with deciding how many creatures to tap
return new HashMap<>(); return new HashMap<>();

View File

@@ -0,0 +1,8 @@
Name:Geyser Leaper
ManaCost:4 U
Types:Creature Human Warrior Ally
PT:4/3
K:Flying
A:AB$ Draw | Cost$ Waterbend<4> | NumCards$ 1 | SubAbility$ DBDiscard | SpellDescription$ Draw a card, then discard a card. (While paying a waterbend cost, you can tap your artifacts and creatures to help. Each one pays for {1}.)
SVar:DBDiscard:DB$ Discard | Defined$ You | NumCards$ 1 | Mode$ TgtChoose
Oracle:Flying\nWaterbend 4: Draw a card, then discard a card.

View File

@@ -0,0 +1,9 @@
Name:Ruinous Waterbending
ManaCost:1 B B
Types:Sorcery Lesson
S:Mode$ OptionalCost | EffectZone$ All | ValidCard$ Card.Self | ValidSA$ Spell | Cost$ Waterbend<4> | Description$ As an additional cost to cast this spell, you may waterbend {4}. (While paying a waterbend cost, you can tap your artifacts and creatures to help. Each one pays for {1}.)
A:SP$ PumpAll | ValidCards$ Creature | NumAtt$ -2 | NumDef$ -2 | IsCurse$ True | SubAbility$ DBEffect | SpellDescription$ All creatures get -2/-2 until end of turn. If this spells additional cost was paid, whenever a creature dies this turn, you gain 1 life.
SVar:DBEffect:DB$ Effect | Triggers$ TrigDies | Condition$ OptionalCost | ConditionOptionalPaid$ True
SVar:TrigDies:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature | Execute$ TrigGainLife | TriggerDescription$ Whenever a creature dies this turn, you gain 1 life.
SVar:TrigGainLife:DB$ GainLife | LifeAmount$ 1
Oracle:As an additional cost to cast this spell, you may waterbend {4}. (While paying a waterbend cost, you can tap your artifacts and creatures to help. Each one pays for {1}.)\nAll creatures get -2/-2 until end of turn. If this spells additional cost was paid, whenever a creature dies this turn, you gain 1 life.

View File

@@ -0,0 +1,10 @@
Name:Waterbender's Restoration
ManaCost:U U
Types:Instant Lesson
S:Mode$ RaiseCost | ValidCard$ Card.Self | Activator$ You | Type$ Spell | Cost$ Waterbend<X> | EffectZone$ All | Description$ As an additional cost to cast this spell, waterbend {X}. (While paying a waterbend cost, you can tap your artifacts and creatures to help. Each one pays for {1}.)
A:SP$ ChangeZone | ValidTgts$ Creature.YouCtrl | Announce$ X | TargetMin$ X | TargetMax$ X | Origin$ Battlefield | Destination$ Exile | TgtPrompt$ Select target creature you control | SubAbility$ DelTrig | RememberChanged$ True | SpellDescription$ Exile X target creatures you control. Return those cards to the battlefield under their owners control at the beginning of the next end step.
SVar:DelTrig:DB$ DelayedTrigger | Mode$ Phase | Phase$ End of Turn | Execute$ TrigReturn | RememberObjects$ RememberedLKI | TriggerDescription$ Return exiled permanent to the battlefield. | SubAbility$ DBCleanup
SVar:TrigReturn:DB$ ChangeZone | Origin$ Exile | Destination$ Battlefield | Defined$ DelayTriggerRememberedLKI
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:X:Count$xPaid
Oracle:As an additional cost to cast this spell, waterbend {X}. (While paying a waterbend cost, you can tap your artifacts and creatures to help. Each one pays for {1}.)\nExile X target creatures you control. Return those cards to the battlefield under their owners control at the beginning of the next end step.

View File

@@ -0,0 +1,7 @@
Name:Waterbending Lesson
ManaCost:3 U
Types:Sorcery Lesson
A:SP$ Draw | NumCards$ 3 | SubAbility$ DBDiscard | SpellDescription$ Draw three cards. Then discard a card unless you waterbend {2}. (While paying a waterbend cost, you can tap your artifacts and creatures to help. Each one pays for {1}.)
SVar:DBDiscard:DB$ Discard | Defined$ You | NumCards$ 1 | Mode$ TgtChoose | UnlessCost$ Waterbend<2> | UnlessPayer$ You
DeckHas:Ability$Discard
Oracle:Draw three cards. Then discard a card unless you waterbend {2}. (While paying a waterbend cost, you can tap your artifacts and creatures to help. Each one pays for {1}.)

View File

@@ -27,18 +27,33 @@ public final class InputSelectCardsForConvokeOrImprovise extends InputSelectMany
private final ManaCostBeingPaid remainingCost; private final ManaCostBeingPaid remainingCost;
private final Player player; private final Player player;
private final CardCollectionView availableCards; private final CardCollectionView availableCards;
private final boolean improvise; private final boolean artifacts;
private final boolean creatures;
private final Integer maxSelectable;
private final String cardType; private final String cardType;
private final String description; private final String description;
public InputSelectCardsForConvokeOrImprovise(final PlayerControllerHuman controller, final Player p, final ManaCost cost, final CardCollectionView untapped, boolean impr, final SpellAbility sa) { public InputSelectCardsForConvokeOrImprovise(final PlayerControllerHuman controller, final Player p, final SpellAbility sa, final ManaCost cost, final CardCollectionView untapped, boolean artifacts, boolean creatures, Integer maxReduction) {
super(controller, 0, Math.min(cost.getCMC(), untapped.size()), sa); super(controller, 0, Math.min(cost.getCMC(), untapped.size()), sa);
remainingCost = new ManaCostBeingPaid(cost); remainingCost = new ManaCostBeingPaid(cost);
player = p; player = p;
availableCards = untapped; availableCards = untapped;
improvise = impr; this.artifacts = artifacts;
cardType = impr ? "artifact" : "creature"; this.creatures = creatures;
description = impr ? "Improvise" : "Convoke"; this.maxSelectable = maxReduction;
if (artifacts && creatures) {
cardType = "artifact or creature";
description = "Waterbend";
} else if (!artifacts && !creatures) {
throw new IllegalArgumentException("At least one of artifacts or creatures must be true");
} else if (creatures) {
cardType = "creature";
description = "Convoke";
} else {
cardType = "artifact";
description = "Improvise";
}
} }
@Override @Override
@@ -49,6 +64,10 @@ public final class InputSelectCardsForConvokeOrImprovise extends InputSelectMany
sb.append(sa.getStackDescription()).append("\n"); sb.append(sa.getStackDescription()).append("\n");
} }
sb.append(TextUtil.concatNoSpace("Choose ", cardType, " to tap for ", description, ".\nRemaining mana cost is ", remainingCost.toString())); sb.append(TextUtil.concatNoSpace("Choose ", cardType, " to tap for ", description, ".\nRemaining mana cost is ", remainingCost.toString()));
if (maxSelectable != null) {
sb.append(". You may select up to ").append(chosenCards.size() - maxSelectable).append(" more ").append(cardType).append("(s).");
}
return sb.toString(); return sb.toString();
} }
@@ -66,10 +85,17 @@ public final class InputSelectCardsForConvokeOrImprovise extends InputSelectMany
onSelectStateChanged(card, false); onSelectStateChanged(card, false);
} }
else { else {
if (maxSelectable != null && chosenCards.size() >= maxSelectable) {
// Should show a different message if there's a max selectable
return false;
}
byte chosenColor; byte chosenColor;
if (improvise) { if (artifacts) {
// Waterbend/Improvise can be paid with colorless mana from artifacts
chosenColor = ManaCostShard.COLORLESS.getColorMask(); chosenColor = ManaCostShard.COLORLESS.getColorMask();
} else { } else {
// Convoke can pay color or generic mana cost from creatures
ColorSet colors = card.getColor(); ColorSet colors = card.getColor();
if (colors.isMulticolor()) { if (colors.isMulticolor()) {
//if card is multicolor, strip out any colors which can't be paid towards remaining cost //if card is multicolor, strip out any colors which can't be paid towards remaining cost
@@ -107,10 +133,6 @@ public final class InputSelectCardsForConvokeOrImprovise extends InputSelectMany
return null; return null;
} }
@Override
protected void onPlayerSelected(final Player player, final ITriggerEvent triggerEvent) {
}
public Map<Card, ManaCostShard> getConvokeMap() { public Map<Card, ManaCostShard> getConvokeMap() {
if (hasCancelled()) { if (hasCancelled()) {
return Maps.newHashMap(); return Maps.newHashMap();

View File

@@ -560,9 +560,7 @@ public class HumanPlay {
} }
CardCollection cardsToDelve = new CardCollection(); CardCollection cardsToDelve = new CardCollection();
if (!effect) { CostAdjustment.adjust(toPay, ability, cardsToDelve, false, effect);
CostAdjustment.adjust(toPay, ability, cardsToDelve, false);
}
Card offering = null; Card offering = null;
Card emerge = null; Card emerge = null;

View File

@@ -2332,9 +2332,9 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
@Override @Override
public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(final SpellAbility sa, final ManaCost manaCost, public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(final SpellAbility sa, final ManaCost manaCost,
final CardCollectionView untappedCards, boolean improvise) { final CardCollectionView untappedCards, boolean artifacts, boolean creatures, Integer maxReduction) {
final InputSelectCardsForConvokeOrImprovise inp = new InputSelectCardsForConvokeOrImprovise(this, player, final InputSelectCardsForConvokeOrImprovise inp = new InputSelectCardsForConvokeOrImprovise(this, player,
manaCost, untappedCards, improvise, sa); sa, manaCost, untappedCards, artifacts, creatures, maxReduction);
inp.showAndWait(); inp.showAndWait();
return inp.getConvokeMap(); return inp.getConvokeMap();
} }