diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index e864ce31dcd..edab1ee7792 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -670,7 +670,11 @@ public class AiController { // This is for playing spells regularly (no Cascade/Ripple etc.) private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) { - if (!ComputerUtilCost.canPayCost(sa, player)) { + boolean xCost = ComputerUtilMana.hasXInAnyCostPart(sa); + + if (!xCost && !ComputerUtilCost.canPayCost(sa, player)) { + // for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check + // when the AI won't even be able to play the spell in the first place (even if it could afford it) return AiPlayDecision.CantAfford; } @@ -678,7 +682,20 @@ public class AiController { return AiPlayDecision.CantPlaySa; } - return canPlaySa(sa); + AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc. + if (canPlay != AiPlayDecision.WillPlay) { + return canPlay; + } + + if (xCost && !ComputerUtilCost.canPayCost(sa, player)) { + // for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made + // on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay) + return AiPlayDecision.CantAfford; + } + + // if we got here, looks like we can play the final cost and we could properly set up and target the API and + // are willing to play the SA + return AiPlayDecision.WillPlay; } public AiPlayDecision canPlaySa(SpellAbility sa) { diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index 3b380c5f679..4c538e5d4d3 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -787,6 +787,7 @@ public class AiCostDecision extends CostDecisionMakerBase { final String sVar = ability.getSVar(amount); if (sVar.equals("XChoice")) { c = AbilityUtils.calculateAmount(source, "ChosenX", ability); + source.setSVar("ChosenX", "Number$" + String.valueOf(c)); } else if (amount.equals("All")) { c = source.getCounters(cost.counter); } else if (sVar.equals("Targeted$CardManaCost")) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index f27d3dc6e77..897be168b58 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -2104,6 +2104,16 @@ public class ComputerUtilCombat { defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true); attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true); + // Damage prevention might come from a static effect + if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) { + if (isCombatDamagePrevented(attacker, blocker, attackerDamage)) { + attackerDamage = 0; + } + if (isCombatDamagePrevented(blocker, attacker, defenderDamage)) { + defenderDamage = 0; + } + } + if (combat != null) { for (Card atkr : combat.getAttackersBlockedBy(blocker)) { if (!atkr.equals(attacker)) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index dd1a4541c33..dcde5ed1b96 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -92,7 +92,7 @@ public class ComputerUtilCost { // value later as the AI decides what to do (in checkApiLogic / checkAiLogic) if (sa != null && sa.hasSVar(remCounter.getAmount())) { final String sVar = sa.getSVar(remCounter.getAmount()); - if (sVar.equals("XChoice")) { + if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) { sa.setSVar("ChosenX", String.valueOf(source.getCounters(type))); } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 02ffc322634..faaae9b1859 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -16,11 +16,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.ApiType; import forge.game.card.*; import forge.game.combat.CombatUtil; -import forge.game.cost.Cost; -import forge.game.cost.CostAdjustment; -import forge.game.cost.CostPartMana; -import forge.game.cost.CostPayEnergy; -import forge.game.cost.CostPayment; +import forge.game.cost.*; import forge.game.mana.Mana; import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaPool; @@ -1560,6 +1556,24 @@ public class ComputerUtilMana { return convoke; } + public static boolean hasXInAnyCostPart(SpellAbility sa) { + boolean xCost = false; + if (sa.getPayCosts() != null) { + for (CostPart p : sa.getPayCosts().getCostParts()) { + if (p instanceof CostPartMana) { + if (((CostPartMana) p).getAmountOfX() > 0) { + xCost = true; + break; + } + } else if (p.getAmount().equals("X")) { + xCost = true; + break; + } + } + } + return xCost; + } + public static int determineMaxAffordableX(Player ai, SpellAbility sa) { if (sa.getPayCosts() == null || sa.getPayCosts().getCostMana() == null) { return -1; diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index 3af7196eacf..3de3a7b20e9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -91,6 +91,8 @@ public class AttachAi extends SpellAbilityAi { } // Don't try to attach an aura to a card which will have protection from the relevant color + // TODO: Fix this not to be dependent on "Protection from Color" wording and to be flexible to account for + // other possibilities like "protection from all colors" etc. Card targeted = sa.getTargets().getFirstTargetedCard(); if (targeted != null && !targeted.getZone().is(ZoneType.Battlefield)) { byte color = sa.getTargets().getFirstTargetedCard().getCurrentState().getColor(); diff --git a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java index 7b4c27a77a5..535b4c34f74 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java @@ -15,6 +15,7 @@ import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; +import forge.util.Aggregates; import java.util.Arrays; import java.util.List; @@ -29,7 +30,7 @@ public class ManaEffectAi extends SpellAbilityAi { */ @Override protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) { - if ("ManaRitual".equals(aiLogic)) { + if (aiLogic.startsWith("ManaRitual")) { return doManaRitualLogic(ai, sa); } else if ("Always".equals(aiLogic)) { return true; @@ -117,7 +118,8 @@ public class ManaEffectAi extends SpellAbilityAi { String produced = sa.getParam("Produced"); byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced); - if ("ChosenX".equals(sa.getParam("Amount")) + int numCounters = 0; + if ("XChoice".equals(host.getSVar("X")) && sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) { CounterType ctrType = CounterType.KI; // Petalmane Baku for (CostPart part : sa.getPayCosts().getCostParts()) { @@ -126,7 +128,11 @@ public class ManaEffectAi extends SpellAbilityAi { break; } } - manaReceived = host.getCounters(ctrType); + numCounters = host.getCounters(ctrType); + manaReceived = numCounters; + if ("ManaRitualBattery".equals(sa.getParam("AILogic"))) { + manaReceived++; // adds an extra mana even if no counters removed + } } int searchCMC = numManaSrcs - selfCost + manaReceived; @@ -196,6 +202,12 @@ public class ManaEffectAi extends SpellAbilityAi { CardPredicates.lessCMC(searchCMC), Predicates.or(CardPredicates.isColorless(), CardPredicates.isColor(producedColor)))); + if ("ManaRitualBattery".equals(sa.getParam("AILogic"))) { + // Don't remove more counters than would be needed to cast everything we want to cast + int maxCtrs = Aggregates.sum(castableSpells, CardPredicates.Accessors.fnGetCmc); + sa.setSVar("ChosenX", "Number$" + Math.min(numCounters, maxCtrs)); + } + // TODO: this will probably still waste the card from time to time. Somehow improve detection of castable material. return castableSpells.size() > 0; } diff --git a/forge-gui/res/cardsfolder/b/black_mana_battery.txt b/forge-gui/res/cardsfolder/b/black_mana_battery.txt index e4eb26d5990..6b158ba4b8a 100644 --- a/forge-gui/res/cardsfolder/b/black_mana_battery.txt +++ b/forge-gui/res/cardsfolder/b/black_mana_battery.txt @@ -2,7 +2,7 @@ Name:Black Mana Battery ManaCost:4 Types:Artifact A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. -A:AB$ Mana | Cost$ T SubCounter | References$ X,Y | Produced$ B | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | SpellDescription$ Add {B}, then add an additional {B} for each charge counter removed this way. +A:AB$ Mana | Cost$ T SubCounter | References$ X,Y | Produced$ B | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {B}, then add an additional {B} for each charge counter removed this way. SVar:Y:Number$1/Plus.ChosenX SVar:X:XChoice #ChosenX SVar created by Cost payment diff --git a/forge-gui/res/cardsfolder/b/blue_mana_battery.txt b/forge-gui/res/cardsfolder/b/blue_mana_battery.txt index d5a592473f8..f8eaa9f6c92 100644 --- a/forge-gui/res/cardsfolder/b/blue_mana_battery.txt +++ b/forge-gui/res/cardsfolder/b/blue_mana_battery.txt @@ -2,7 +2,7 @@ Name:Blue Mana Battery ManaCost:4 Types:Artifact A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. -A:AB$ Mana | Cost$ T SubCounter | Produced$ U | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | References$ X,Y | SpellDescription$ Add {U}, then add an additional {U} for each charge counter removed this way. +A:AB$ Mana | Cost$ T SubCounter | Produced$ U | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | References$ X,Y | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {U}, then add an additional {U} for each charge counter removed this way. SVar:Y:Number$1/Plus.ChosenX SVar:X:XChoice #ChosenX SVar created by Cost payment diff --git a/forge-gui/res/cardsfolder/g/green_mana_battery.txt b/forge-gui/res/cardsfolder/g/green_mana_battery.txt index 24c156d0ea2..69a72d9d9e1 100644 --- a/forge-gui/res/cardsfolder/g/green_mana_battery.txt +++ b/forge-gui/res/cardsfolder/g/green_mana_battery.txt @@ -2,7 +2,7 @@ Name:Green Mana Battery ManaCost:4 Types:Artifact A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. -A:AB$ Mana | Cost$ T SubCounter | Produced$ G | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | References$ X,Y | SpellDescription$ Add {G}, then add an additional {G} for each charge counter removed this way. +A:AB$ Mana | Cost$ T SubCounter | Produced$ G | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | References$ X,Y | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {G}, then add an additional {G} for each charge counter removed this way. SVar:Y:Number$1/Plus.ChosenX SVar:X:XChoice #ChosenX SVar created by Cost payment diff --git a/forge-gui/res/cardsfolder/r/red_mana_battery.txt b/forge-gui/res/cardsfolder/r/red_mana_battery.txt index b6c2b5319fd..770054db1ca 100644 --- a/forge-gui/res/cardsfolder/r/red_mana_battery.txt +++ b/forge-gui/res/cardsfolder/r/red_mana_battery.txt @@ -2,7 +2,7 @@ Name:Red Mana Battery ManaCost:4 Types:Artifact A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. -A:AB$ Mana | Cost$ T SubCounter | Produced$ R | Amount$ Y | References$ X,Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | SpellDescription$ Add {R}, then add an additional {R} for each charge counter removed this way. +A:AB$ Mana | Cost$ T SubCounter | Produced$ R | Amount$ Y | References$ X,Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {R}, then add an additional {R} for each charge counter removed this way. SVar:Y:Number$1/Plus.ChosenX SVar:X:XChoice #ChosenX SVar created by Cost payment diff --git a/forge-gui/res/cardsfolder/w/white_mana_battery.txt b/forge-gui/res/cardsfolder/w/white_mana_battery.txt index f1ac19742e2..280c4e35d52 100644 --- a/forge-gui/res/cardsfolder/w/white_mana_battery.txt +++ b/forge-gui/res/cardsfolder/w/white_mana_battery.txt @@ -2,7 +2,7 @@ Name:White Mana Battery ManaCost:4 Types:Artifact A:AB$ PutCounter | Cost$ 2 T | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on CARDNAME. -A:AB$ Mana | Cost$ T SubCounter | Produced$ W | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | SpellDescription$ Add {W}, then add an additional {W} for each charge counter removed this way. +A:AB$ Mana | Cost$ T SubCounter | Produced$ W | Amount$ Y | CostDesc$ {T}, Remove any number of charge counters from CARDNAME: | AILogic$ ManaRitualBattery | AINoRecursiveCheck$ True | SpellDescription$ Add {W}, then add an additional {W} for each charge counter removed this way. SVar:Y:Number$1/Plus.ChosenX SVar:X:XChoice #ChosenX SVar created by Cost payment