DestroyAI: better logic for Pongify also Update for X

This commit is contained in:
Hans Mackowiak
2020-12-25 23:23:06 +01:00
parent 60bf6dde9d
commit d8f010d54a
6 changed files with 214 additions and 167 deletions

View File

@@ -1,6 +1,10 @@
package forge.ai; package forge.ai;
import java.util.List;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ability.TokenAi; import forge.ai.ability.TokenAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -12,8 +16,6 @@ import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
/* /*
@@ -31,60 +33,83 @@ public class SpecialAiLogic {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
Game game = source.getGame(); Game game = source.getGame();
PhaseHandler ph = game.getPhaseHandler(); 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); List<Card> targetable = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa);
listOpp = CardLists.getTargetableCards(listOpp, 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; Card choice = null;
if (token == null || !token.isCreature() || token.getNetToughness() < 1) { if (!listOpp.isEmpty()) {
return true; // becomes Terminate choice = ComputerUtilCard.getMostExpensivePermanentAI(listOpp);
} else if (choice != null && choice.isPlaneswalker()) { // can choice even be null?
if (choice.getCurrentLoyalty() * 35 > ComputerUtilCard.evaluateCreature(token)) {
sa.resetTargets(); if (choice != null) {
sa.getTargets().add(choice); final Card token = TokenAi.spawnToken(choice.getController(), tokenSA);
return true; if (!token.isCreature() || token.getNetToughness() < 1) {
} else { sa.resetTargets();
return false; 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);
} }
if (bestOwnCardToUpgrade == null) {
// See if we have anything we can upgrade bestOwnCardToUpgrade = ComputerUtilCard.getWorstCreatureAI(CardLists.filter(listOwn, new Predicate<Card>() {
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<Card>() {
@Override @Override
public boolean apply(Card card) { public boolean apply(Card card) {
return card.isCreature() && (ComputerUtilCard.isUselessCreature(ai, card) return card.isCreature() && (ComputerUtilCard.isUselessCreature(ai, card)
|| ComputerUtilCard.evaluateCreature(token) > 2 * ComputerUtilCard.evaluateCreature(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;
} }
if (bestOwnCardToUpgrade != null) {
return hasOppTarget; 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" // A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT"

View File

@@ -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)) { if (cost != null && !willPayCosts(ai, sa, cost, source)) {
return false; return false;
} }
return checkApiLogic(ai, sa); return true;
} }
protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) { protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) {

View File

@@ -13,7 +13,6 @@ import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
public class DestroyAi extends SpellAbilityAi { public class DestroyAi extends SpellAbilityAi {
@@ -23,89 +22,19 @@ public class DestroyAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean canPlayAI(final Player ai, SpellAbility sa) { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
// 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();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final boolean noRegen = sa.hasParam("NoRegen"); if (sa.usesTargeting()) {
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) {
sa.resetTargets(); sa.resetTargets();
if (sa.hasParam("TargetingPlayer")) { if ("MadSarkhanDragon".equals(aiLogic)) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa);
}
if ("MadSarkhanDragon".equals(logic)) {
return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa); return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa);
} else if (logic != null && logic.startsWith("MinLoyalty.")) { } else if (aiLogic.startsWith("MinLoyalty.")) {
int minLoyalty = Integer.parseInt(logic.substring(logic.indexOf(".") + 1)); int minLoyalty = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
if (source.getCounters(CounterEnumType.LOYALTY) < minLoyalty) { if (source.getCounters(CounterEnumType.LOYALTY) < minLoyalty) {
return false; return false;
} }
} else if ("Polymorph".equals(logic)) { } else if ("Polymorph".equals(aiLogic)) {
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;
} }
@@ -124,7 +53,108 @@ public class DestroyAi extends SpellAbilityAi {
} }
sa.getTargets().add(worst); sa.getTargets().add(worst);
return true; 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); list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
if ("FatalPush".equals(logic)) { if ("FatalPush".equals(logic)) {
final int cmcMax = ai.hasRevolt() ? 4 : 2; final int cmcMax = ai.hasRevolt() ? 4 : 2;
@@ -184,33 +214,12 @@ public class DestroyAi extends SpellAbilityAi {
return false; 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 // target loop
// TODO use can add more Targets
while (sa.getTargets().size() < maxTargets) { while (sa.getTargets().size() < maxTargets) {
if (list.isEmpty()) { if (list.isEmpty()) {
if ((sa.getTargets().size() < abTgt.getMinTargets(sa.getHostCard(), sa)) if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|| (sa.getTargets().size() == 0)) {
sa.resetTargets(); sa.resetTargets();
return false; return false;
} else { } else {
@@ -222,10 +231,6 @@ public class DestroyAi extends SpellAbilityAi {
Card choice = null; Card choice = null;
// If the targets are only of one type, take the best // If the targets are only of one type, take the best
if (CardLists.getNotType(list, "Creature").isEmpty()) { if (CardLists.getNotType(list, "Creature").isEmpty()) {
if ("Pongify".equals(logic)) {
return SpecialAiLogic.doPongifyLogic(ai, sa);
}
choice = ComputerUtilCard.getBestCreatureAI(list); choice = ComputerUtilCard.getBestCreatureAI(list);
if ("OppDestroyYours".equals(logic)) { if ("OppDestroyYours".equals(logic)) {
Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay()); Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay());
@@ -246,15 +251,14 @@ public class DestroyAi extends SpellAbilityAi {
choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true); choice = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
} }
//option to hold removal instead only applies for single targeted removal //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)) { if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) {
return false; return false;
} }
} }
if (choice == null) { // can't find anything left if (choice == null) { // can't find anything left
if ((sa.getTargets().size() < abTgt.getMinTargets(sa.getHostCard(), sa)) if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|| (sa.getTargets().size() == 0)) {
sa.resetTargets(); sa.resetTargets();
return false; return false;
} else { } else {
@@ -298,22 +302,19 @@ public class DestroyAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { 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"); final boolean noRegen = sa.hasParam("NoRegen");
if (tgt != null) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); 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 // Try to avoid targeting creatures that are dead on board
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa); 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); CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents()); preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
if (CardLists.getNotType(preferred, "Creature").isEmpty()) { if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
@@ -344,10 +345,9 @@ public class DestroyAi extends SpellAbilityAi {
return false; return false;
} }
while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) { while (sa.canAddMoreTarget()) {
if (preferred.isEmpty()) { if (preferred.isEmpty()) {
if (sa.getTargets().size() == 0 if (!sa.isMinTargetChosen()) {
|| sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (!mandatory) { if (!mandatory) {
sa.resetTargets(); sa.resetTargets();
return false; 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()) { if (list.isEmpty()) {
break; break;
} else { } else {
@@ -392,7 +392,7 @@ public class DestroyAi extends SpellAbilityAi {
} }
} }
return sa.getTargets().size() >= tgt.getMinTargets(sa.getHostCard(), sa); return sa.isTargetNumberValid();
} else { } else {
return mandatory; return mandatory;
} }

View File

@@ -65,6 +65,16 @@ public class CostDiscard extends CostPartWithList {
public int paymentOrder() { return 10; } 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) * (non-Javadoc)
* *

View File

@@ -1391,7 +1391,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return false; return false;
} }
return getTargets().size() < getTargetRestrictions().getMaxTargets(hostCard, this); return getTargets().size() < getMaxTargets();
} }
public boolean isZeroTargets() { public boolean isZeroTargets() {
@@ -1405,6 +1405,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return getTargetRestrictions().isMaxTargetsChosen(hostCard, this); return getTargetRestrictions().isMaxTargetsChosen(hostCard, this);
} }
public int getMinTargets() {
return getTargetRestrictions().getMinTargets(getHostCard(), this);
}
public int getMaxTargets() {
return getTargetRestrictions().getMaxTargets(getHostCard(), this);
}
public boolean isTargetNumberValid() { public boolean isTargetNumberValid() {
if (!this.usesTargeting()) { if (!this.usesTargeting()) {
return getTargets().isEmpty(); return getTargets().isEmpty();
@@ -1413,7 +1421,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
if (!isMinTargetChosen()) { if (!isMinTargetChosen()) {
return false; return false;
} }
int maxTargets = getTargetRestrictions().getMaxTargets(hostCard, this); int maxTargets = getMaxTargets();
if (maxTargets == 0 && getPayCosts().hasSpecificCostType(CostRemoveCounter.class) if (maxTargets == 0 && getPayCosts().hasSpecificCostType(CostRemoveCounter.class)
&& hasSVar(getParam("TargetMax")) && hasSVar(getParam("TargetMax"))

View File

@@ -1,7 +1,7 @@
Name:Scorched Earth Name:Scorched Earth
ManaCost:X R ManaCost:X R
Types:Sorcery Types:Sorcery
A:SP$ Destroy | Cost$ X R Discard<X/Land/land card(s)> | 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<X/Land/land card(s)> | 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 SVar:X:Count$xPaid
AI:RemoveDeck:Random AI:RemoveDeck:Random
SVar:PlayBeforeLandDrop:true SVar:PlayBeforeLandDrop:true