From d8f010d54a788ec14ef51ccf0c5d5412543adaa1 Mon Sep 17 00:00:00 2001 From: Hans Mackowiak Date: Fri, 25 Dec 2020 23:23:06 +0100 Subject: [PATCH] DestroyAI: better logic for Pongify also Update for X --- .../main/java/forge/ai/SpecialAiLogic.java | 109 +++++--- .../main/java/forge/ai/SpellAbilityAi.java | 6 +- .../main/java/forge/ai/ability/DestroyAi.java | 242 +++++++++--------- .../java/forge/game/cost/CostDiscard.java | 10 + .../forge/game/spellability/SpellAbility.java | 12 +- .../res/cardsfolder/s/scorched_earth.txt | 2 +- 6 files changed, 214 insertions(+), 167 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java index 6a378ea6d0f..c757133f256 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -1,6 +1,10 @@ package forge.ai; +import java.util.List; + import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; + import forge.ai.ability.TokenAi; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -12,8 +16,6 @@ import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; -import forge.game.zone.ZoneType; import forge.util.Aggregates; /* @@ -31,60 +33,83 @@ public class SpecialAiLogic { Card source = sa.getHostCard(); Game game = source.getGame(); PhaseHandler ph = game.getPhaseHandler(); - TargetRestrictions tgt = sa.getTargetRestrictions(); + boolean isDestroy = ApiType.Destroy.equals(sa.getApi()); + SpellAbility tokenSA = sa.findSubAbilityByType(ApiType.Token); + if (tokenSA == null) { + // Used wrong AI logic? + return false; + } - CardCollection listOpp = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, source, sa); - listOpp = CardLists.getTargetableCards(listOpp, sa); + List targetable = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa); - Card choice = ComputerUtilCard.getMostExpensivePermanentAI(listOpp); + CardCollection listOpp = CardLists.filterControlledBy(targetable, ai.getOpponents()); + if (isDestroy) { + listOpp = CardLists.getNotKeyword(listOpp, Keyword.INDESTRUCTIBLE); + // TODO add handling for cards like targeting dies + } - final Card token = choice != null ? TokenAi.spawnToken(choice.getController(), sa.getSubAbility()) : null; - if (token == null || !token.isCreature() || token.getNetToughness() < 1) { - return true; // becomes Terminate - } else if (choice != null && choice.isPlaneswalker()) { - if (choice.getCurrentLoyalty() * 35 > ComputerUtilCard.evaluateCreature(token)) { - sa.resetTargets(); - sa.getTargets().add(choice); - return true; - } else { - return false; + Card choice = null; + if (!listOpp.isEmpty()) { + choice = ComputerUtilCard.getMostExpensivePermanentAI(listOpp); + // can choice even be null? + + if (choice != null) { + final Card token = TokenAi.spawnToken(choice.getController(), tokenSA); + if (!token.isCreature() || token.getNetToughness() < 1) { + sa.resetTargets(); + sa.getTargets().add(choice); + return true; + } + if (choice.isPlaneswalker()) { + if (choice.getCurrentLoyalty() * 35 > ComputerUtilCard.evaluateCreature(token)) { + sa.resetTargets(); + sa.getTargets().add(choice); + return true; + } else { + return false; + } + } + if ((!choice.isCreature() || choice.isTapped()) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.isPlayerTurn(ai) // prevent surprise combatant + || ComputerUtilCard.evaluateCreature(choice) < 1.5 * ComputerUtilCard.evaluateCreature(token)) { + choice = null; + } } - } else { - boolean hasOppTarget = true; - if (choice != null - && ((!choice.isCreature() || choice.isTapped()) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.getPlayerTurn() == ai) // prevent surprise combatant - || ComputerUtilCard.evaluateCreature(choice) < 1.5 * ComputerUtilCard.evaluateCreature(token)) { + } - hasOppTarget = false; + // See if we have anything we can upgrade + if (choice == null) { + CardCollection listOwn = CardLists.filterControlledBy(targetable, ai); + final Card token = TokenAi.spawnToken(ai, tokenSA); + + Card bestOwnCardToUpgrade = null; + if (isDestroy) { + // just choose any Indestructible + // TODO maybe filter something that doesn't like to be targeted, or does something benefit by targeting + bestOwnCardToUpgrade = Iterables.getFirst(CardLists.getKeyword(listOwn, Keyword.INDESTRUCTIBLE), null); } - - // See if we have anything we can upgrade - if (!hasOppTarget) { - CardCollection listOwn = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, source, sa); - listOwn = CardLists.getTargetableCards(listOwn, sa); - - Card bestOwnCardToUpgrade = ComputerUtilCard.getWorstCreatureAI(CardLists.filter(listOwn, new Predicate() { + if (bestOwnCardToUpgrade == null) { + bestOwnCardToUpgrade = ComputerUtilCard.getWorstCreatureAI(CardLists.filter(listOwn, new Predicate() { @Override public boolean apply(Card card) { return card.isCreature() && (ComputerUtilCard.isUselessCreature(ai, card) || ComputerUtilCard.evaluateCreature(token) > 2 * ComputerUtilCard.evaluateCreature(card)); } })); - if (bestOwnCardToUpgrade != null) { - if (ComputerUtilCard.isUselessCreature(ai, bestOwnCardToUpgrade) || (ph.getPhase().isAfter(PhaseType.COMBAT_END) || ph.getPlayerTurn() != ai)) { - sa.resetTargets(); - sa.getTargets().add(bestOwnCardToUpgrade); - return true; - } - } - } else { - sa.resetTargets(); - sa.getTargets().add(choice); - return true; } - - return hasOppTarget; + if (bestOwnCardToUpgrade != null) { + if (ComputerUtilCard.isUselessCreature(ai, bestOwnCardToUpgrade) || (ph.getPhase().isAfter(PhaseType.COMBAT_END) || !ph.isPlayerTurn(ai))) { + choice = bestOwnCardToUpgrade; + } + } } + + if (choice != null) { + sa.resetTargets(); + sa.getTargets().add(choice); + return true; + } + + return false; } // A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT" diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index 6b78b69b223..fa33c11087a 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -78,10 +78,14 @@ public abstract class SpellAbilityAi { } } + if (!checkApiLogic(ai, sa)) { + return false; + } + // needs to be after API logic because needs to check possible X Cost? if (cost != null && !willPayCosts(ai, sa, cost, source)) { return false; } - return checkApiLogic(ai, sa); + return true; } protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java index 33108116b0c..1c8c72f6e32 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -13,7 +13,6 @@ import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; public class DestroyAi extends SpellAbilityAi { @@ -23,89 +22,19 @@ public class DestroyAi extends SpellAbilityAi { } @Override - protected boolean canPlayAI(final Player ai, SpellAbility sa) { - // AI needs to be expanded, since this function can be pretty complex - // based on what the expected targets could be - final Cost abCost = sa.getPayCosts(); - final TargetRestrictions abTgt = sa.getTargetRestrictions(); + protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { final Card source = sa.getHostCard(); - final boolean noRegen = sa.hasParam("NoRegen"); - final String logic = sa.getParam("AILogic"); - boolean hasXCost = false; - - CardCollection list; - - if (abCost != null) { - if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) { - return false; - } - - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; - } - - if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) { - return false; - } - - hasXCost = sa.costHasManaX(); - } - - if ("AtOpponentsCombatOrAfter".equals(sa.getParam("AILogic"))) { - PhaseHandler ph = ai.getGame().getPhaseHandler(); - if (ph.getPlayerTurn() == ai || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { - return false; - } - } else if ("AtEOT".equals(sa.getParam("AILogic"))) { - PhaseHandler ph = ai.getGame().getPhaseHandler(); - if (!ph.is(PhaseType.END_OF_TURN)) { - return false; - } - } else if ("AtEOTIfNotAttacking".equals(sa.getParam("AILogic"))) { - PhaseHandler ph = ai.getGame().getPhaseHandler(); - if (!ph.is(PhaseType.END_OF_TURN) || !ai.getCreaturesAttackedThisTurn().isEmpty()) { - return false; - } - } - - if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; - } - - // Ability that's intended to destroy own useless token to trigger Grave Pacts - // should be fired at end of turn or when under attack after blocking to make opponent sac something - boolean havepact = false; - - // TODO replace it with look for a dies -> sacrifice trigger check - havepact |= ai.isCardInPlay("Grave Pact"); - havepact |= ai.isCardInPlay("Butcher of Malakir"); - havepact |= ai.isCardInPlay("Dictate of Erebos"); - if ("Pactivator".equals(logic) && havepact) { - if ((!ai.getGame().getPhaseHandler().isPlayerTurn(ai)) - && ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) || (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))) - && (ai.getOpponents().getCreaturesInPlay().size() > 0)) { - ai.getController().chooseTargetsFor(sa); - return true; - } - } - - // Targeting - if (abTgt != null) { + if (sa.usesTargeting()) { sa.resetTargets(); - if (sa.hasParam("TargetingPlayer")) { - Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); - sa.setTargetingPlayer(targetingPlayer); - return targetingPlayer.getController().chooseTargetsFor(sa); - } - if ("MadSarkhanDragon".equals(logic)) { + if ("MadSarkhanDragon".equals(aiLogic)) { return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa); - } else if (logic != null && logic.startsWith("MinLoyalty.")) { - int minLoyalty = Integer.parseInt(logic.substring(logic.indexOf(".") + 1)); + } else if (aiLogic.startsWith("MinLoyalty.")) { + int minLoyalty = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1)); if (source.getCounters(CounterEnumType.LOYALTY) < minLoyalty) { return false; } - } else if ("Polymorph".equals(logic)) { - list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); + } else if ("Polymorph".equals(aiLogic)) { + CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty()) { return false; } @@ -124,7 +53,108 @@ public class DestroyAi extends SpellAbilityAi { } sa.getTargets().add(worst); return true; + } else if ("Pongify".equals(aiLogic)) { + return SpecialAiLogic.doPongifyLogic(ai, sa); } + } + return super.checkAiLogic(ai, sa, aiLogic); + } + + protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph, + final String logic) { + if ("AtOpponentsCombatOrAfter".equals(logic)) { + if (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { + return false; + } + } else if ("AtEOT".equals(logic)) { + if (!ph.is(PhaseType.END_OF_TURN)) { + return false; + } + } else if ("AtEOTIfNotAttacking".equals(logic)) { + if (!ph.is(PhaseType.END_OF_TURN) || !ai.getCreaturesAttackedThisTurn().isEmpty()) { + return false; + } + } else if ("Pactivator".equals(logic)) { + // Ability that's intended to destroy own useless token to trigger Grave Pacts + // should be fired at end of turn or when under attack after blocking to make opponent sac something + boolean havepact = false; + + // TODO replace it with look for a dies -> sacrifice trigger check + havepact |= ai.isCardInPlay("Grave Pact"); + havepact |= ai.isCardInPlay("Butcher of Malakir"); + havepact |= ai.isCardInPlay("Dictate of Erebos"); + if (havepact) { + if ((!ph.isPlayerTurn(ai)) + && ((ph.is(PhaseType.END_OF_TURN)) || (ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS))) + && (ai.getOpponents().getCreaturesInPlay().size() > 0)) { + CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); + Card worst = ComputerUtilCard.getWorstAI(list); + if (worst != null) { + sa.getTargets().add(worst); + return true; + } + return false; + } + } + } + + return true; + } + + @Override + protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + final Card source = sa.getHostCard(); + final boolean noRegen = sa.hasParam("NoRegen"); + final String logic = sa.getParam("AILogic"); + + CardCollection list; + + + + if (ComputerUtil.preventRunAwayActivations(sa)) { + return false; + } + + + // Targeting + if (sa.usesTargeting()) { + // Assume there where already enough targets chosen by AI Logic Above + if (!sa.canAddMoreTarget() && sa.isTargetNumberValid()) { + return true; + } + + // reset targets before AI Logic part + sa.resetTargets(); + int maxTargets; + + if (sa.costHasManaX()) { + // TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement. + maxTargets = ComputerUtilCost.getMaxXValue(sa, ai); + // need to set XPaid to get the right number for + sa.setXManaCostPaid(maxTargets); + // need to check for maxTargets + maxTargets = Math.min(maxTargets, sa.getMaxTargets()); + } else { + maxTargets = sa.getMaxTargets(); + } + if (sa.hasParam("AIMaxTgtsCount")) { + // Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified + // TODO: is there a better way to predict things like "sac X" costs without needing a special AI variable? + maxTargets = Math.min(CardFactoryUtil.xCount(sa.getHostCard(), "Count$" + sa.getParam("AIMaxTgtsCount")), maxTargets); + } + + if (maxTargets == 0) { + // can't afford X or otherwise target anything + return false; + } + + if (sa.hasParam("TargetingPlayer")) { + Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); + sa.setTargetingPlayer(targetingPlayer); + return targetingPlayer.getController().chooseTargetsFor(sa); + } + + // AI doesn't destroy own cards if it isn't defined in AI logic list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa); if ("FatalPush".equals(logic)) { final int cmcMax = ai.hasRevolt() ? 4 : 2; @@ -184,33 +214,12 @@ public class DestroyAi extends SpellAbilityAi { return false; } - int maxTargets = abTgt.getMaxTargets(sa.getHostCard(), sa); - - if (hasXCost) { - // TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement. - maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa)); - // X can't be more than the lands we have in our hand for "discard X lands"! - if ("ScorchedEarth".equals(logic)) { - int lands = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size(); - maxTargets = Math.min(maxTargets, lands); - } - } - if (sa.hasParam("AIMaxTgtsCount")) { - // Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified - // TODO: is there a better way to predict things like "sac X" costs without needing a special AI variable? - maxTargets = Math.min(CardFactoryUtil.xCount(sa.getHostCard(), "Count$" + sa.getParam("AIMaxTgtsCount")), maxTargets); - } - - if (maxTargets == 0) { - // can't afford X or otherwise target anything - return false; - } // target loop + // TODO use can add more Targets while (sa.getTargets().size() < maxTargets) { if (list.isEmpty()) { - if ((sa.getTargets().size() < abTgt.getMinTargets(sa.getHostCard(), sa)) - || (sa.getTargets().size() == 0)) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { sa.resetTargets(); return false; } else { @@ -222,10 +231,6 @@ public class DestroyAi extends SpellAbilityAi { Card choice = null; // If the targets are only of one type, take the best if (CardLists.getNotType(list, "Creature").isEmpty()) { - if ("Pongify".equals(logic)) { - return SpecialAiLogic.doPongifyLogic(ai, sa); - } - choice = ComputerUtilCard.getBestCreatureAI(list); if ("OppDestroyYours".equals(logic)) { Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay()); @@ -246,15 +251,14 @@ public class DestroyAi extends SpellAbilityAi { choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true); } //option to hold removal instead only applies for single targeted removal - if (!sa.isTrigger() && abTgt.getMaxTargets(sa.getHostCard(), sa) == 1) { + if (!sa.isTrigger() && sa.getMaxTargets() == 1) { if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) { return false; } } if (choice == null) { // can't find anything left - if ((sa.getTargets().size() < abTgt.getMinTargets(sa.getHostCard(), sa)) - || (sa.getTargets().size() == 0)) { + if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { sa.resetTargets(); return false; } else { @@ -298,22 +302,19 @@ public class DestroyAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - final TargetRestrictions tgt = sa.getTargetRestrictions(); - final Card source = sa.getHostCard(); final boolean noRegen = sa.hasParam("NoRegen"); - if (tgt != null) { + if (sa.usesTargeting()) { sa.resetTargets(); CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); - list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); + + if (list.isEmpty() || list.size() < sa.getMinTargets()) { + return false; + } // Try to avoid targeting creatures that are dead on board list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa); - if (list.isEmpty() || list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) { - return false; - } - CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE); preferred = CardLists.filterControlledBy(preferred, ai.getOpponents()); if (CardLists.getNotType(preferred, "Creature").isEmpty()) { @@ -344,10 +345,9 @@ public class DestroyAi extends SpellAbilityAi { return false; } - while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) { + while (sa.canAddMoreTarget()) { if (preferred.isEmpty()) { - if (sa.getTargets().size() == 0 - || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (!sa.isMinTargetChosen()) { if (!mandatory) { sa.resetTargets(); return false; @@ -371,7 +371,7 @@ public class DestroyAi extends SpellAbilityAi { } } - while (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { + while (sa.canAddMoreTarget()) { if (list.isEmpty()) { break; } else { @@ -392,7 +392,7 @@ public class DestroyAi extends SpellAbilityAi { } } - return sa.getTargets().size() >= tgt.getMinTargets(sa.getHostCard(), sa); + return sa.isTargetNumberValid(); } else { return mandatory; } diff --git a/forge-game/src/main/java/forge/game/cost/CostDiscard.java b/forge-game/src/main/java/forge/game/cost/CostDiscard.java index 3edd069cfde..a51f5409a38 100644 --- a/forge-game/src/main/java/forge/game/cost/CostDiscard.java +++ b/forge-game/src/main/java/forge/game/cost/CostDiscard.java @@ -65,6 +65,16 @@ public class CostDiscard extends CostPartWithList { public int paymentOrder() { return 10; } + @Override + public Integer getMaxAmountX(SpellAbility ability, Player payer) { + final Card source = ability.getHostCard(); + String type = this.getType(); + CardCollectionView handList = payer.canDiscardBy(ability) ? payer.getCardsIn(ZoneType.Hand) : CardCollection.EMPTY; + + handList = CardLists.getValidCards(handList, type.split(";"), payer, source, ability); + return handList.size(); + } + /* * (non-Javadoc) * 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 0f9d07fcf00..d9e04293f5e 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -1391,7 +1391,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return false; } - return getTargets().size() < getTargetRestrictions().getMaxTargets(hostCard, this); + return getTargets().size() < getMaxTargets(); } public boolean isZeroTargets() { @@ -1405,6 +1405,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return getTargetRestrictions().isMaxTargetsChosen(hostCard, this); } + public int getMinTargets() { + return getTargetRestrictions().getMinTargets(getHostCard(), this); + } + + public int getMaxTargets() { + return getTargetRestrictions().getMaxTargets(getHostCard(), this); + } + public boolean isTargetNumberValid() { if (!this.usesTargeting()) { return getTargets().isEmpty(); @@ -1413,7 +1421,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit if (!isMinTargetChosen()) { return false; } - int maxTargets = getTargetRestrictions().getMaxTargets(hostCard, this); + int maxTargets = getMaxTargets(); if (maxTargets == 0 && getPayCosts().hasSpecificCostType(CostRemoveCounter.class) && hasSVar(getParam("TargetMax")) diff --git a/forge-gui/res/cardsfolder/s/scorched_earth.txt b/forge-gui/res/cardsfolder/s/scorched_earth.txt index 291ca501892..60bb6747e7f 100644 --- a/forge-gui/res/cardsfolder/s/scorched_earth.txt +++ b/forge-gui/res/cardsfolder/s/scorched_earth.txt @@ -1,7 +1,7 @@ Name:Scorched Earth ManaCost:X R Types:Sorcery -A:SP$ Destroy | Cost$ X R Discard | CostDesc$ As an additional cost to cast this spell, discard X land cards. | TargetMin$ X | TargetMax$ X | ValidTgts$ Land | TgtPrompt$ Select X target lands | References$ X | SpellDescription$ Destroy X target lands. | AILogic$ ScorchedEarth +A:SP$ Destroy | Cost$ X R Discard | CostDesc$ As an additional cost to cast this spell, discard X land cards. | TargetMin$ X | TargetMax$ X | ValidTgts$ Land | TgtPrompt$ Select X target lands | References$ X | SpellDescription$ Destroy X target lands. SVar:X:Count$xPaid AI:RemoveDeck:Random SVar:PlayBeforeLandDrop:true