Compare commits

...

3 Commits

Author SHA1 Message Date
Fulgur14
40e9480c2c Apostrophes correction (#9130)
* Update ruinous_waterbending.txt

* Update waterbenders_restoration.txt
2025-11-10 14:33:39 +01:00
tool4ever
3db276f323 Secret of Bloodbending and support (#9129) 2025-11-10 12:18:53 +00:00
Chris H
c9a5fe9135 Initial checkin for Waterbending (#9120) 2025-11-10 10:27:44 +00:00
25 changed files with 212 additions and 59 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

@@ -75,11 +75,13 @@ public class Game {
private List<Card> activePlanes = null; private List<Card> activePlanes = null;
public final Phase cleanup;
public final Phase endOfCombat;
public final Phase endOfTurn;
public final Untap untap; public final Untap untap;
public final Phase upkeep; public final Phase upkeep;
public final Phase beginOfCombat;
public final Phase endOfCombat;
public final Phase endOfTurn;
public final Phase cleanup;
// to execute commands for "current" phase each time state based action is checked // to execute commands for "current" phase each time state based action is checked
public final List<GameCommand> sbaCheckedCommandList; public final List<GameCommand> sbaCheckedCommandList;
public final MagicStack stack; public final MagicStack stack;
@@ -363,9 +365,10 @@ public class Game {
untap = new Untap(this); untap = new Untap(this);
upkeep = new Phase(PhaseType.UPKEEP); upkeep = new Phase(PhaseType.UPKEEP);
cleanup = new Phase(PhaseType.CLEANUP); beginOfCombat = new Phase(PhaseType.COMBAT_BEGIN);
endOfCombat = new Phase(PhaseType.COMBAT_END); endOfCombat = new Phase(PhaseType.COMBAT_END);
endOfTurn = new Phase(PhaseType.END_OF_TURN); endOfTurn = new Phase(PhaseType.END_OF_TURN);
cleanup = new Phase(PhaseType.CLEANUP);
sbaCheckedCommandList = new ArrayList<>(); sbaCheckedCommandList = new ArrayList<>();
@@ -428,6 +431,9 @@ public class Game {
public final Phase getUpkeep() { public final Phase getUpkeep() {
return upkeep; return upkeep;
} }
public final Phase getBeginOfCombat() {
return beginOfCombat;
}
public final Phase getEndOfCombat() { public final Phase getEndOfCombat() {
return endOfCombat; return endOfCombat;
} }

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

@@ -2,7 +2,6 @@ package forge.game.ability.effects;
import java.util.List; import java.util.List;
import forge.GameCommand;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
@@ -28,10 +27,11 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Player controller = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Controller"), sa).get(0); final Player controller = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Controller"), sa).get(0);
final Game game = controller.getGame(); final Game game = controller.getGame();
final boolean combat = sa.hasParam("Combat");
for (final Player pTarget: getTargetPlayers(sa)) { for (final Player pTarget: getTargetPlayers(sa)) {
// before next untap gain control // before next untap gain control
game.getCleanup().addUntil(pTarget, (GameCommand) () -> { (combat ? game.getBeginOfCombat() : game.getCleanup()).addUntil(pTarget, () -> {
// CR 800.4b // CR 800.4b
if (!controller.isInGame()) { if (!controller.isInGame()) {
return; return;
@@ -41,7 +41,7 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
pTarget.addController(ts, controller); pTarget.addController(ts, controller);
// after following cleanup release control // after following cleanup release control
game.getCleanup().addUntil((GameCommand) () -> pTarget.removeController(ts)); (combat ? game.getEndOfCombat() : game.getCleanup()).addUntil(() -> pTarget.removeController(ts));
}); });
} }
} }

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

@@ -297,6 +297,7 @@ public class PhaseHandler implements java.io.Serializable {
case COMBAT_BEGIN: case COMBAT_BEGIN:
nCombatsThisTurn++; nCombatsThisTurn++;
combat = new Combat(playerTurn); combat = new Combat(playerTurn);
game.getBeginOfCombat().executeUntil(playerTurn);
//PhaseUtil.verifyCombat(); //PhaseUtil.verifyCombat();
break; break;
@@ -756,7 +757,6 @@ public class PhaseHandler implements java.io.Serializable {
} }
} }
} }
// fire blockers declared trigger
final Map<AbilityKey, Object> bdRunParams = AbilityKey.newMap(); final Map<AbilityKey, Object> bdRunParams = AbilityKey.newMap();
bdRunParams.put(AbilityKey.Blockers, declaredBlockers); bdRunParams.put(AbilityKey.Blockers, declaredBlockers);
bdRunParams.put(AbilityKey.Attackers, blockedAttackers); bdRunParams.put(AbilityKey.Attackers, blockedAttackers);
@@ -768,7 +768,6 @@ public class PhaseHandler implements java.io.Serializable {
continue; continue;
} }
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Blocker, c1); runParams.put(AbilityKey.Blocker, c1);
runParams.put(AbilityKey.Attackers, combat.getAttackersBlockedBy(c1)); runParams.put(AbilityKey.Attackers, combat.getAttackersBlockedBy(c1));

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

@@ -3,6 +3,6 @@ ManaCost:1 G
Types:Sorcery Types:Sorcery
A:SP$ Pump | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ DBDamage | AILogic$ PowerDmg | StackDescription$ {c:ThisTargetedCard} | SpellDescription$ Target creature you control A:SP$ Pump | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ DBDamage | AILogic$ PowerDmg | StackDescription$ {c:ThisTargetedCard} | SpellDescription$ Target creature you control
SVar:DBDamage:DB$ DealDamage | ValidTgts$ Creature | TgtPrompt$ Select another target creature | TargetUnique$ True | AILogic$ PowerDmg | NumDmg$ X | DamageSource$ ParentTarget | ExcessSVar$ Excess | SubAbility$ DBDiscover | StackDescription$ REP another target creature_{c:ThisTargetedCard} | SpellDescription$ deals damage equal to its power to another target creature. SVar:DBDamage:DB$ DealDamage | ValidTgts$ Creature | TgtPrompt$ Select another target creature | TargetUnique$ True | AILogic$ PowerDmg | NumDmg$ X | DamageSource$ ParentTarget | ExcessSVar$ Excess | SubAbility$ DBDiscover | StackDescription$ REP another target creature_{c:ThisTargetedCard} | SpellDescription$ deals damage equal to its power to another target creature.
SVar:DBDiscover:DB$ Discover | Num$ Excess | StackDescription$ SpellDescription | SpellDescription$ If excess damage was dealt this way, discover X, where X is that excess damage. (Exile cards from the top of your library until you exile a nonland card with that mana value or less. Cast it without paying its mana cost or put it into your hand. Put the rest on the bottom in a random order.) SVar:DBDiscover:DB$ Discover | Num$ Excess | ConditionCheckSVar$ Excess | StackDescription$ SpellDescription | SpellDescription$ If excess damage was dealt this way, discover X, where X is that excess damage. (Exile cards from the top of your library until you exile a nonland card with that mana value or less. Cast it without paying its mana cost or put it into your hand. Put the rest on the bottom in a random order.)
SVar:X:ParentTargeted$CardPower SVar:X:ParentTargeted$CardPower
Oracle:Target creature you control deals damage equal to its power to another target creature. If excess damage was dealt this way, discover X, where X is that excess damage. (Exile cards from the top of your library until you exile a nonland card with that mana value or less. Cast it without paying its mana cost or put it into your hand. Put the rest on the bottom in a random order.) Oracle:Target creature you control deals damage equal to its power to another target creature. If excess damage was dealt this way, discover X, where X is that excess damage. (Exile cards from the top of your library until you exile a nonland card with that mana value or less. Cast it without paying its mana cost or put it into your hand. Put the rest on the bottom in a random order.)

View File

@@ -2,6 +2,6 @@ Name:Of One Mind
ManaCost:2 U ManaCost:2 U
Types:Sorcery Types:Sorcery
S:Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ 2 | EffectZone$ All | IsPresent$ Creature.Human+YouCtrl | CheckSVar$ Count$Valid Creature.nonHuman+YouCtrl | Description$ CARDNAME costs {2} less to cast if you control a Human creature and a non-Human creature. S:Mode$ ReduceCost | ValidCard$ Card.Self | Type$ Spell | Amount$ 2 | EffectZone$ All | IsPresent$ Creature.Human+YouCtrl | CheckSVar$ Count$Valid Creature.nonHuman+YouCtrl | Description$ CARDNAME costs {2} less to cast if you control a Human creature and a non-Human creature.
DeckHints:Type$Human
A:SP$ Draw | NumCards$ 2 | SpellDescription$ Draw two cards. A:SP$ Draw | NumCards$ 2 | SpellDescription$ Draw two cards.
DeckHints:Type$Human
Oracle:This spell costs {2} less to cast if you control a Human creature and a non-Human creature.\nDraw two cards. Oracle:This spell costs {2} less to cast if you control a Human creature and a non-Human creature.\nDraw two cards.

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

View File

@@ -0,0 +1,9 @@
Name:Secret of Bloodbending
ManaCost:U U U U
Types:Sorcery Lesson
S:Mode$ OptionalCost | EffectZone$ All | ValidCard$ Card.Self | ValidSA$ Spell | Cost$ Waterbend<10> | Description$ As an additional cost to cast this spell, you may waterbend {10}.
A:SP$ ControlPlayer | ValidTgts$ Opponent | Condition$ OptionalCost | ConditionOptionalPaid$ False | Combat$ True | SubAbility$ ControlTurn | SpellDescription$ You control target opponent during their next combat phase. If this spells additional cost was paid, you control that player during their next turn instead. Exile CARDNAME.
SVar:ControlTurn:DB$ ControlPlayer | Defined$ Targeted | Condition$ OptionalCost | ConditionOptionalPaid$ True | SubAbility$ DBExile
SVar:DBExile:DB$ ChangeZone | Defined$ Self | Origin$ Stack | Destination$ Exile
SVar:X:Count$xPaid
Oracle:As an additional cost to cast this spell, you may waterbend {10}.\nYou control target opponent during their next combat phase. If this spells additional cost was paid, you control that player during their next turn instead. (You see all cards that player could see and make all decisions for them.)\nExile Secret of Bloodbending.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Human Pilot Ally
PT:1/4 PT:1/4
K:Flying K:Flying
T:Mode$ AttackersDeclared | AttackingPlayer$ You | ValidAttackers$ Creature.withFlying | Execute$ TrigDraw | TriggerZones$ Battlefield | TriggerDescription$ Whenever one or more creatures you control with flying attack, draw a card, then discard a card. When you discard a nonland card this way, put a +1/+1 counter on target creature you control. T:Mode$ AttackersDeclared | AttackingPlayer$ You | ValidAttackers$ Creature.withFlying | Execute$ TrigDraw | TriggerZones$ Battlefield | TriggerDescription$ Whenever one or more creatures you control with flying attack, draw a card, then discard a card. When you discard a nonland card this way, put a +1/+1 counter on target creature you control.
SVar:DBDraw:DB$ Draw | SubAbility$ DBDiscard | SpellDescription$ Draw a card, then discard a card. SVar:TrigDraw:DB$ Draw | SubAbility$ DBDiscard
SVar:DBDiscard:DB$ Discard | Defined$ You | NumCards$ 1 | Mode$ TgtChoose | RememberDiscarded$ True | SubAbility$ DBImmediateTrig SVar:DBDiscard:DB$ Discard | Defined$ You | NumCards$ 1 | Mode$ TgtChoose | RememberDiscarded$ True | SubAbility$ DBImmediateTrig
SVar:DBImmediateTrig:DB$ ImmediateTrigger | ConditionDefined$ Remembered | ConditionPresent$ Card.nonLand | ConditionCompare$ GE1 | Execute$ TrigPutCounter | SubAbility$ DBCleanup | SpellDescription$ When you discard a nonland card this way, put a +1/+1 counter on target creature you control. SVar:DBImmediateTrig:DB$ ImmediateTrigger | ConditionDefined$ Remembered | ConditionPresent$ Card.nonLand | ConditionCompare$ GE1 | Execute$ TrigPutCounter | SubAbility$ DBCleanup | SpellDescription$ When you discard a nonland card this way, put a +1/+1 counter on target creature you control.
SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | CounterType$ P1P1 | CounterNum$ 1 SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | CounterType$ P1P1 | CounterNum$ 1

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

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