From c9a5fe9135c8ca208e0beaead4e78eeeca066b95 Mon Sep 17 00:00:00 2001 From: Chris H Date: Mon, 10 Nov 2025 05:27:44 -0500 Subject: [PATCH] Initial checkin for Waterbending (#9120) --- .../main/java/forge/ai/ComputerUtilMana.java | 17 +++--- .../java/forge/ai/PlayerControllerAi.java | 12 ++-- .../java/forge/game/ability/AbilityUtils.java | 2 + .../src/main/java/forge/game/cost/Cost.java | 15 +++++ .../java/forge/game/cost/CostAdjustment.java | 55 +++++++++++++------ .../java/forge/game/cost/CostPartMana.java | 11 +++- .../java/forge/game/cost/CostWaterbend.java | 18 ++++++ .../forge/game/player/PlayerController.java | 2 +- .../forge/game/spellability/SpellAbility.java | 11 ++++ .../main/java/forge/game/zone/MagicStack.java | 4 ++ .../util/PlayerControllerForTests.java | 2 +- .../cardsfolder/upcoming/geyser_leaper.txt | 8 +++ .../upcoming/ruinous_waterbending.txt | 9 +++ .../upcoming/waterbenders_restoration.txt | 10 ++++ .../upcoming/waterbending_lesson.txt | 7 +++ ...InputSelectCardsForConvokeOrImprovise.java | 42 ++++++++++---- .../src/main/java/forge/player/HumanPlay.java | 4 +- .../forge/player/PlayerControllerHuman.java | 4 +- 18 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 forge-game/src/main/java/forge/game/cost/CostWaterbend.java create mode 100644 forge-gui/res/cardsfolder/upcoming/geyser_leaper.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/ruinous_waterbending.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/waterbenders_restoration.txt create mode 100644 forge-gui/res/cardsfolder/upcoming/waterbending_lesson.txt diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index cb96486b256..5a5b248916a 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -1345,9 +1345,7 @@ public class ComputerUtilMana { } } - if (!effect) { - CostAdjustment.adjust(manaCost, sa, null, test); - } + CostAdjustment.adjust(manaCost, sa, null, test, effect); if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle ManaCost mkCost = sa.getPayCosts().getTotalMana(); @@ -1773,15 +1771,18 @@ public class ComputerUtilMana { /** * Matches list of creatures to shards in mana cost for convoking. - * @param cost cost of convoked ability - * @param list creatures to be evaluated - * @param improvise + * + * @param cost cost of convoked ability + * @param list creatures to be evaluated + * @param artifacts + * @param creatures * @return map between creatures and shards to convoke */ - public static Map getConvokeOrImproviseFromList(final ManaCost cost, List list, boolean improvise) { + public static Map getConvokeOrImproviseFromList(final ManaCost cost, List list, boolean artifacts, boolean creatures) { final Map convoke = new HashMap<>(); Card convoked = null; - if (!improvise) { + if (creatures && !artifacts) { + // Run for convoke but not improvise or waterbending for (ManaCostShard toPay : cost) { if (toPay.isSnow() || toPay.isColorless()) { continue; diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 53336b7b63a..5bc00fcea00 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -1382,7 +1382,7 @@ public class PlayerControllerAi extends PlayerController { } @Override - public Map chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean improvise) { + public Map chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean artifacts, boolean creatures, Integer maxReduction) { final Player ai = sa.getActivatingPlayer(); final PhaseHandler ph = ai.getGame().getPhaseHandler(); //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 if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { - if (improvise) { + if (!creatures) { untapped = CardLists.filter(untapped, c -> !c.isCreature()); } else { + // TODO AI needs to learn how to use Convoke or Waterbend return new HashMap<>(); } } @@ -1406,13 +1407,16 @@ public class PlayerControllerAi extends PlayerController { if (!ai.getGame().getStack().isEmpty()) { final List objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), null); for (Card c : blockers) { - if (objects.contains(c) && (!improvise || c.isArtifact())) { + if (objects.contains(c) && (creatures || c.isArtifact())) { untapped.add(c); } + if (maxReduction != null && untapped.size() >= maxReduction) { + break; + } } } } - return ComputerUtilMana.getConvokeOrImproviseFromList(manaCost, untapped, improvise); + return ComputerUtilMana.getConvokeOrImproviseFromList(manaCost, untapped, artifacts, creatures); } @Override diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 04003aa5d43..d58dc28e237 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -15,6 +15,7 @@ import forge.game.*; import forge.game.ability.AbilityFactory.AbilityRecordType; import forge.game.card.*; import forge.game.cost.Cost; +import forge.game.cost.CostAdjustment; import forge.game.cost.IndividualCostPaymentInstance; import forge.game.keyword.Keyword; import forge.game.keyword.KeywordInterface; @@ -1527,6 +1528,7 @@ public class AbilityUtils { else { cost = new Cost(unlessCost, true); } + cost = CostAdjustment.adjust(cost, sa, true); return cost; } diff --git a/forge-game/src/main/java/forge/game/cost/Cost.java b/forge-game/src/main/java/forge/game/cost/Cost.java index 8cf784c6e7e..3c6275c9624 100644 --- a/forge-game/src/main/java/forge/game/cost/Cost.java +++ b/forge-game/src/main/java/forge/game/cost/Cost.java @@ -188,6 +188,15 @@ public class Cost implements Serializable { return this.isAbility; } + public final String getMaxWaterbend() { + for (CostPart cp : this.costParts) { + if (cp instanceof CostPartMana) { + return ((CostPartMana) cp).getMaxWaterbend(); + } + } + return null; + } + private Cost() { } @@ -564,6 +573,11 @@ public class Cost implements Serializable { 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")) { return new CostForage(); } @@ -973,6 +987,7 @@ public class Cost implements Serializable { } else { 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 (part instanceof CostDiscard || part instanceof CostDraw || part instanceof CostAddMana || part instanceof CostPayLife || diff --git a/forge-game/src/main/java/forge/game/cost/CostAdjustment.java b/forge-game/src/main/java/forge/game/cost/CostAdjustment.java index ed0a6bda460..8653452ba2f 100644 --- a/forge-game/src/main/java/forge/game/cost/CostAdjustment.java +++ b/forge-game/src/main/java/forge/game/cost/CostAdjustment.java @@ -31,11 +31,13 @@ import org.apache.commons.lang3.StringUtils; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.function.Predicate; public class CostAdjustment { public static Cost adjust(final Cost cost, final SpellAbility sa, boolean effect) { if (sa.isTrigger() || cost == null || effect) { + sa.setMaxWaterbend(cost); return cost; } @@ -99,6 +101,9 @@ public class CostAdjustment { host.setState(CardStateName.Original, false); host.setFaceDown(false); } + + sa.setMaxWaterbend(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. // 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) { - if (sa.isTrigger() || sa.isReplacementAbility()) { + public static boolean adjust(ManaCostBeingPaid cost, final SpellAbility sa, CardCollection cardsToDelveOut, boolean test, boolean effect) { + if (effect) { + adjustCostByWaterbend(cost, sa, test); + } + if (effect || sa.isTrigger() || sa.isReplacementAbility()) { return true; } final Game game = sa.getActivatingPlayer().getGame(); final Card originalCard = sa.getHostCard(); - boolean isStateChangeToFaceDown = false; - if (sa.isSpell()) { - if (sa.isCastFaceDown() && !originalCard.isFaceDown()) { - // Turn face down to apply cost modifiers correctly - originalCard.turnFaceDownNoUpdate(); - isStateChangeToFaceDown = true; - } + boolean isStateChangeToFaceDown = false; + if (sa.isSpell() && sa.isCastFaceDown() && !originalCard.isFaceDown()) { + // Turn face down to apply cost modifiers correctly + originalCard.turnFaceDownNoUpdate(); + isStateChangeToFaceDown = true; } CardCollection cardsOnBattlefield = new CardCollection(game.getCardsIn(ZoneType.Battlefield)); @@ -278,17 +284,19 @@ public class CostAdjustment { table.triggerChangesZoneAll(game, sa); } if (sa.getHostCard().hasKeyword(Keyword.CONVOKE)) { - adjustCostByConvokeOrImprovise(cost, sa, false, test); + adjustCostByConvokeOrImprovise(cost, sa, false, true, test); } if (sa.getHostCard().hasKeyword(Keyword.IMPROVISE)) { - adjustCostByConvokeOrImprovise(cost, sa, true, test); + adjustCostByConvokeOrImprovise(cost, sa, true, false, test); } } // isSpell if (sa.hasParam("TapCreaturesForMana")) { - adjustCostByConvokeOrImprovise(cost, sa, false, test); + adjustCostByConvokeOrImprovise(cost, sa, false, true, test); } + adjustCostByWaterbend(cost, sa, test); + // Reset card state (if changed) if (isStateChangeToFaceDown) { originalCard.setFaceDown(false); @@ -299,6 +307,13 @@ public class CostAdjustment { } // 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) { // 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. @@ -321,27 +336,33 @@ public class CostAdjustment { return assistant.getController().helpPayForAssistSpell(cost, sa, genericLeft, requestedAmount); } - private static void adjustCostByConvokeOrImprovise(ManaCostBeingPaid cost, final SpellAbility sa, boolean improvise, boolean test) { - if (!improvise) { + private static void adjustCostByConvokeOrImprovise(ManaCostBeingPaid cost, final SpellAbility sa, boolean artifacts, boolean creatures, boolean test) { + if (creatures && !artifacts) { sa.clearTappedForConvoke(); } final Player activator = sa.getActivatingPlayer(); CardCollectionView untappedCards = CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.CAN_TAP); - if (improvise) { + + Integer maxReduction = null; + if (artifacts && creatures) { + maxReduction = sa.getMaxWaterbend(); + Predicate isArtifactOrCreature = card -> card.isArtifact() || card.isCreature(); + untappedCards = CardLists.filter(untappedCards, isArtifactOrCreature); + } else if (artifacts) { untappedCards = CardLists.filter(untappedCards, CardPredicates.ARTIFACTS); } else { untappedCards = CardLists.filter(untappedCards, CardPredicates.CREATURES); } Map convokedCards = activator.getController().chooseCardsForConvokeOrImprovise(sa, - cost.toManaCost(), untappedCards, improvise); + cost.toManaCost(), untappedCards, artifacts, creatures, maxReduction); CardCollection tapped = new CardCollection(); for (final Entry conv : convokedCards.entrySet()) { Card c = conv.getKey(); - if (!improvise) { + if (creatures && !artifacts) { sa.addTappedForConvoke(c); } cost.decreaseShard(conv.getValue(), 1); diff --git a/forge-game/src/main/java/forge/game/cost/CostPartMana.java b/forge-game/src/main/java/forge/game/cost/CostPartMana.java index 9c9cd8b15e7..b31ce92ecc9 100644 --- a/forge-game/src/main/java/forge/game/cost/CostPartMana.java +++ b/forge-game/src/main/java/forge/game/cost/CostPartMana.java @@ -39,6 +39,8 @@ public class CostPartMana extends CostPart { private boolean isEnchantedCreatureCost = false; private boolean isCostPayAnyNumberOfTimes = false; + protected String maxWaterbend; + public int paymentOrder() { return shouldPayLast() ? 200 : 0; } public boolean shouldPayLast() { @@ -63,6 +65,13 @@ public class CostPartMana extends CostPart { this.isEnchantedCreatureCost = enchantedCreatureCost; } + public String getMaxWaterbend() { + return maxWaterbend; + } + public void setMaxWaterbend(String max) { + maxWaterbend = max; + } + /** * Gets the mana. * @@ -101,7 +110,7 @@ public class CostPartMana extends CostPart { public boolean isUndoable() { return true; } @Override - public final String toString() { + public String toString() { return cost.toString(); } diff --git a/forge-game/src/main/java/forge/game/cost/CostWaterbend.java b/forge-game/src/main/java/forge/game/cost/CostWaterbend.java new file mode 100644 index 00000000000..bf9c2b3c933 --- /dev/null +++ b/forge-game/src/main/java/forge/game/cost/CostWaterbend.java @@ -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(); + } +} diff --git a/forge-game/src/main/java/forge/game/player/PlayerController.java b/forge-game/src/main/java/forge/game/player/PlayerController.java index b6eff922da9..0e745e0cd3b 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerController.java +++ b/forge-game/src/main/java/forge/game/player/PlayerController.java @@ -202,7 +202,7 @@ public abstract class PlayerController { public abstract CardCollection chooseCardsToDiscardToMaximumHandSize(int numDiscard); public abstract CardCollectionView chooseCardsToDelve(int genericAmount, CardCollection grave); - public abstract Map chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean improvise); + public abstract Map chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean artifacts, boolean creatures, Integer maxReduction); public abstract List chooseCardsForSplice(SpellAbility sa, List cards); public abstract CardCollectionView chooseCardsToRevealFromHand(int min, int max, CardCollectionView valid); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 61cbd70160a..6e4d782f858 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -144,6 +144,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private final Supplier tappedForConvoke = Suppliers.memoize(CardCollection::new); private Card sacrificedAsOffering; private Card sacrificedAsEmerge; + private Integer maxWaterbend; private AbilityManaPart manaPart; @@ -2692,4 +2693,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit public void setName(String 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); + } } diff --git a/forge-game/src/main/java/forge/game/zone/MagicStack.java b/forge-game/src/main/java/forge/game/zone/MagicStack.java index 37f0eec264b..c4010f3521b 100644 --- a/forge-game/src/main/java/forge/game/zone/MagicStack.java +++ b/forge-game/src/main/java/forge/game/zone/MagicStack.java @@ -428,6 +428,10 @@ public class MagicStack /* extends MyObservable */ implements Iterable 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 // Probably along with deciding how many creatures to tap return new HashMap<>(); diff --git a/forge-gui/res/cardsfolder/upcoming/geyser_leaper.txt b/forge-gui/res/cardsfolder/upcoming/geyser_leaper.txt new file mode 100644 index 00000000000..505c7194087 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/geyser_leaper.txt @@ -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. diff --git a/forge-gui/res/cardsfolder/upcoming/ruinous_waterbending.txt b/forge-gui/res/cardsfolder/upcoming/ruinous_waterbending.txt new file mode 100644 index 00000000000..04749581310 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/ruinous_waterbending.txt @@ -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 spell’s 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 spell’s additional cost was paid, whenever a creature dies this turn, you gain 1 life. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/upcoming/waterbenders_restoration.txt b/forge-gui/res/cardsfolder/upcoming/waterbenders_restoration.txt new file mode 100644 index 00000000000..1bef0c72b42 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/waterbenders_restoration.txt @@ -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 | 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 owner’s 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 owner’s control at the beginning of the next end step. diff --git a/forge-gui/res/cardsfolder/upcoming/waterbending_lesson.txt b/forge-gui/res/cardsfolder/upcoming/waterbending_lesson.txt new file mode 100644 index 00000000000..d2b873101e5 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/waterbending_lesson.txt @@ -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}.) diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectCardsForConvokeOrImprovise.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectCardsForConvokeOrImprovise.java index e1b5cf960ef..39afa242b1e 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectCardsForConvokeOrImprovise.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputSelectCardsForConvokeOrImprovise.java @@ -27,18 +27,33 @@ public final class InputSelectCardsForConvokeOrImprovise extends InputSelectMany private final ManaCostBeingPaid remainingCost; private final Player player; 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 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); remainingCost = new ManaCostBeingPaid(cost); player = p; availableCards = untapped; - improvise = impr; - cardType = impr ? "artifact" : "creature"; - description = impr ? "Improvise" : "Convoke"; + this.artifacts = artifacts; + this.creatures = creatures; + 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 @@ -49,6 +64,10 @@ public final class InputSelectCardsForConvokeOrImprovise extends InputSelectMany sb.append(sa.getStackDescription()).append("\n"); } 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(); } @@ -66,10 +85,17 @@ public final class InputSelectCardsForConvokeOrImprovise extends InputSelectMany onSelectStateChanged(card, false); } else { + if (maxSelectable != null && chosenCards.size() >= maxSelectable) { + // Should show a different message if there's a max selectable + return false; + } + byte chosenColor; - if (improvise) { + if (artifacts) { + // Waterbend/Improvise can be paid with colorless mana from artifacts chosenColor = ManaCostShard.COLORLESS.getColorMask(); } else { + // Convoke can pay color or generic mana cost from creatures ColorSet colors = card.getColor(); if (colors.isMulticolor()) { //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; } - @Override - protected void onPlayerSelected(final Player player, final ITriggerEvent triggerEvent) { - } - public Map getConvokeMap() { if (hasCancelled()) { return Maps.newHashMap(); diff --git a/forge-gui/src/main/java/forge/player/HumanPlay.java b/forge-gui/src/main/java/forge/player/HumanPlay.java index 009f552a28d..0c8196ae4ad 100644 --- a/forge-gui/src/main/java/forge/player/HumanPlay.java +++ b/forge-gui/src/main/java/forge/player/HumanPlay.java @@ -560,9 +560,7 @@ public class HumanPlay { } CardCollection cardsToDelve = new CardCollection(); - if (!effect) { - CostAdjustment.adjust(toPay, ability, cardsToDelve, false); - } + CostAdjustment.adjust(toPay, ability, cardsToDelve, false, effect); Card offering = null; Card emerge = null; diff --git a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java index c8b59f60c97..d754aadc2c3 100644 --- a/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java +++ b/forge-gui/src/main/java/forge/player/PlayerControllerHuman.java @@ -2332,9 +2332,9 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont @Override public Map 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, - manaCost, untappedCards, improvise, sa); + sa, manaCost, untappedCards, artifacts, creatures, maxReduction); inp.showAndWait(); return inp.getConvokeMap(); }