From 8e0bc63a8b83d90e6f0d6232baddedebed1e0986 Mon Sep 17 00:00:00 2001 From: Agetian Date: Sun, 5 Nov 2023 22:21:11 +0300 Subject: [PATCH] AI framework to improve sacrificing endangered cards + several AI hints (Stoneforge Mystic, Atog, others) and improvements. (#4014) * - Implement a fallback mechanism in case getting a card by name and edition fails for whatever reason. * - Patch up pulling a card without filters in All Card Variants mode. * - Sacrifice creatures when they're endangered (currently works for AF LifeGain, LifeLose, and any AFs that do not have phase-based AI restrictions or other factors that will prevent instant speed activation) * - Tweaks to the requirements for the AI. - Some AI enablement. * - Account for non-creature endangered objects * - Flag Wall of Limbs as RemAIDeck for now. * - Support for AF PutCounter. * - Clean up. * - Logic fix for AF PutCounter. * - Clean up. * - Logic tweak/fix for AF Pump. * - Another slight tweak. * - Some AI hint fixes/additions. * - Some AI hint fixes/additions. * - Improve timing for AF LifeGain/LifeLose. * - AI profile option for default SacCost AI preference. * - Default Sacrifice AI preference master toggle. * - Stoneforge Mystic AI hint. * - For now, keep the default pref SacCost toggle to the Experimental AI and at minimum values (too extreme for general use). * - AI hint: Cryptbreaker * - Don't auto-sac creatures that evaluate above a given threshold, sac smaller CMC first * - Lower the priority of cards that have a self-sacrifice activated ability * - Revert the evaluation modification until a better solution is found. * - AI hint for Hallowed Moonlight. * - AI hint for Winds of Abandon (AI casts the non-overloaded version in Main 1, so cast the other one in Main 1 as well to be able to prioritize/choose) * - AI logic for The One Ring. * - Some logic tweaks/fixes. * - Winds of Abandon: use the AI logic hint like other similar non-permanent spells. * - Fix logic for default sacrifice priorities. - Mark P9 Mox, Black Lotus, and Lotus Petal cards as bad for AI sacrifice. * - More logic fixes. * - One more logic fix. * - Revert the AIDontSacrifice hint for now. * - Revert Tinker as well * - Limit LifeLoseAi sac logic to threatened cards. * - Logic tweak. * - Logic tweak. * - Simplify check (part already checked above). * - Some more minor cleanup. * - AI shouldn't sacrifice things mid-combat in presence of Trample or Banding because of altered combat rules (likely to backfire/result in a misplay) - Minor cleanup. * - A [hacky] way to make the AI understand Anticognition and Bring the Ending. * - Fix imports. * - Avoid a crash by ensuring that the AI parameter indeed points to an AI player (and not e.g. predicting/simulating human decisions at the moment) * - Do not try to sacrifice a card in an attempt to regenerate it * - Clean up for AiController mustRespond * - Suppress recursive checkSacrificeCost when called from the predictive code. - Trample only matters for the attacking side when checking for threatened card SacCost requirements * - Naming convention. * - NPE guard. * - Recommended tweaks and fixes. * - Don't override X payment for a triggered ability (e.g. Spiteful Banditry) * - A better attempt at handling X inside trigger code. * - Process AI logic for EffectAi from triggered abilities. * - Improve Black Lotus AI by handling it as if it were a Mana Ritual card when processing ManaEffectAi. * - AI property guarded check + meaningful default for potential non-AI calls --- .../src/main/java/forge/ai/AiController.java | 9 +- forge-ai/src/main/java/forge/ai/AiProps.java | 7 +- .../src/main/java/forge/ai/ComputerUtil.java | 104 ++++++++++++++---- .../java/forge/ai/ComputerUtilCombat.java | 39 +++++++ .../main/java/forge/ai/ComputerUtilCost.java | 16 ++- .../src/main/java/forge/ai/SpecialCardAi.java | 16 +++ .../main/java/forge/ai/ability/BranchAi.java | 42 ++++++- .../java/forge/ai/ability/ChangeZoneAi.java | 2 + .../forge/ai/ability/ChangeZoneAllAi.java | 23 ++-- .../java/forge/ai/ability/CountersPutAi.java | 19 +++- .../java/forge/ai/ability/DamageAllAi.java | 3 +- .../java/forge/ai/ability/DamageDealAi.java | 2 +- .../main/java/forge/ai/ability/DrawAi.java | 14 ++- .../main/java/forge/ai/ability/EffectAi.java | 8 ++ .../java/forge/ai/ability/LifeGainAi.java | 16 ++- .../java/forge/ai/ability/LifeLoseAi.java | 10 +- .../java/forge/ai/ability/ManaEffectAi.java | 11 +- forge-gui/res/ai/Cautious.ai | 13 +++ forge-gui/res/ai/Default.ai | 14 +++ forge-gui/res/ai/Experimental.ai | 16 ++- forge-gui/res/ai/Reckless.ai | 13 +++ .../res/cardsfolder/a/animal_boneyard.txt | 1 - forge-gui/res/cardsfolder/a/anticognition.txt | 3 +- .../arguels_blood_fast_temple_of_aclazotz.txt | 2 +- forge-gui/res/cardsfolder/a/atog.txt | 1 + .../res/cardsfolder/b/bone_splinters.txt | 2 +- .../res/cardsfolder/b/bring_the_ending.txt | 2 +- .../res/cardsfolder/c/corrupted_harvester.txt | 2 +- forge-gui/res/cardsfolder/c/cryptbreaker.txt | 2 +- .../res/cardsfolder/d/diamond_valley.txt | 1 - .../cardsfolder/d/disciple_of_griselbrand.txt | 1 - .../res/cardsfolder/h/hallowed_moonlight.txt | 3 +- forge-gui/res/cardsfolder/h/high_market.txt | 1 - .../res/cardsfolder/h/hurler_cyclops.txt | 2 +- .../res/cardsfolder/k/kheru_dreadmaw.txt | 1 - .../cardsfolder/m/miren_the_moaning_well.txt | 1 - .../res/cardsfolder/m/mutual_destruction.txt | 2 +- .../cardsfolder/rebalanced/a-the_one_ring.txt | 2 +- .../res/cardsfolder/s/stoneforge_mystic.txt | 2 +- forge-gui/res/cardsfolder/t/the_one_ring.txt | 2 +- .../res/cardsfolder/v/vampiric_rites.txt | 1 - forge-gui/res/cardsfolder/w/wall_of_limbs.txt | 2 +- .../res/cardsfolder/w/winds_of_abandon.txt | 2 +- 43 files changed, 344 insertions(+), 91 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index bb3118a44b8..13fc609c4f7 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1537,7 +1537,14 @@ public class AiController { top = game.getStack().peekAbility(); } final boolean topOwnedByAI = top != null && top.getActivatingPlayer().equals(player); - final boolean mustRespond = top != null && top.hasParam("AIRespondsToOwnAbility"); + + // Must respond: cases where the AI should respond to its own triggers or other abilities (need to add negative stuff to be countered here) + boolean mustRespond = false; + if (top != null) { + mustRespond = top.hasParam("AIRespondsToOwnAbility"); // Forced combos (currently defined for Sensei's Divining Top) + mustRespond |= top.isTrigger() && top.getTrigger().getKeyword() != null + && top.getTrigger().getKeyword().getKeyword() == Keyword.EVOKE; // Evoke sacrifice trigger + } if (topOwnedByAI) { // AI's own spell: should probably let my stuff resolve first, but may want to copy the SA or respond to it diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java index ee162987ba9..2edeacba908 100644 --- a/forge-ai/src/main/java/forge/ai/AiProps.java +++ b/forge-ai/src/main/java/forge/ai/AiProps.java @@ -134,7 +134,12 @@ public enum AiProps { /** */ FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK("100"), BLINK_RELOAD_PLANESWALKER_CHANCE("30"), /** */ BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY("2"), /** */ - BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF("2"); /** */ + BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF("2"), + SACRIFICE_DEFAULT_PREF_ENABLE("true"), + SACRIFICE_DEFAULT_PREF_MIN_CMC("0"), + SACRIFICE_DEFAULT_PREF_MAX_CMC("2"), + SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS("true"), + SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL("135"); // Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting // <-- There are no experimental options here --> diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 2e0d107e1dd..dafcfc78802 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import forge.game.cost.*; import org.apache.commons.lang3.StringUtils; import com.google.common.base.Predicate; @@ -65,13 +66,6 @@ import forge.game.card.CounterEnumType; import forge.game.card.CounterType; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; -import forge.game.cost.Cost; -import forge.game.cost.CostDiscard; -import forge.game.cost.CostExile; -import forge.game.cost.CostPart; -import forge.game.cost.CostPayment; -import forge.game.cost.CostPutCounter; -import forge.game.cost.CostSacrifice; import forge.game.keyword.Keyword; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -433,12 +427,41 @@ public class ComputerUtil { final CardCollection sacMeList = CardLists.filter(typeList, new Predicate() { @Override public boolean apply(final Card c) { - return c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) == priority; + return (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) == priority) + || (priority == 1 && shouldSacrificeThreatenedCard(ai, c, sa)); } }); if (!sacMeList.isEmpty()) { CardLists.shuffle(sacMeList); - return sacMeList.get(0); + return sacMeList.getFirst(); + } else { + // empty sacMeList, so get some viable average preference if the option is enabled + if (ai.getController().isAI()) { + AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); + boolean enableDefaultPref = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ENABLE); + if (enableDefaultPref) { + int minCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MIN_CMC); + int maxCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CMC); + int maxCreatureEval = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL); + boolean allowTokens = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS); + List dontSac = Arrays.asList("Black Lotus", "Mox Pearl", "Mox Jet", "Mox Emerald", "Mox Ruby", "Mox Sapphire", "Lotus Petal"); + CardCollection allowList = CardLists.filter(typeList, new Predicate() { + @Override + public boolean apply(Card card) { + if (card.isCreature() && ComputerUtilCard.evaluateCreature(card) > maxCreatureEval) { + return false; + } + + return (allowTokens && card.isToken()) + || (card.getCMC() >= minCMC && card.getCMC() <= maxCMC && !dontSac.contains(card.getName())); + } + }); + if (!allowList.isEmpty()) { + CardLists.sortByCmcDesc(allowList); + return allowList.getLast(); + } + } + } } } @@ -1435,6 +1458,8 @@ public class ComputerUtil { if (type.equals("CARDNAME")) { if (source.getSVar("SacMe").equals("6")) { return true; + } else if (shouldSacrificeThreatenedCard(ai, source, sa)) { + return true; } continue; } @@ -1444,6 +1469,8 @@ public class ComputerUtil { for (Card c : typeList) { if (c.getSVar("SacMe").equals("6")) { return true; + } else if (shouldSacrificeThreatenedCard(ai, c, sa)) { + return true; } } } @@ -1810,6 +1837,13 @@ public class ComputerUtil { } if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) { + if (saviour.usesTargeting() && !saviour.canTarget(c)) { + continue; + } else if (saviour.getPayCosts() != null && saviour.getPayCosts().hasSpecificCostType(CostSacrifice.class) + && (!ComputerUtilCost.isSacrificeSelfCost(saviour.getPayCosts())) || c == source) { + continue; + } + boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source, false) < ComputerUtilCombat.getDamageToKill(c, false); if ((!topStack.usesTargeting() && !grantIndestructible && !canSave) || (!grantIndestructible && !grantShroud && !canSave)) { @@ -1818,6 +1852,13 @@ public class ComputerUtil { } if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) { + if (saviour.usesTargeting() && !saviour.canTarget(c)) { + continue; + } else if (saviour.getPayCosts() != null && saviour.getPayCosts().hasSpecificCostType(CostSacrifice.class) + && (!ComputerUtilCost.isSacrificeSelfCost(saviour.getPayCosts())) || c == source) { + continue; + } + boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source, false) < ComputerUtilCombat.getDamageToKill(c, false); if (!canSave) { continue; @@ -2038,25 +2079,35 @@ public class ComputerUtil { * @return true if the creature dies according to current board position. */ public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature, final SpellAbility excludeSa) { + return predictCreatureWillDieThisTurn(ai, creature, excludeSa, false); + } + + public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature, final SpellAbility excludeSa, final boolean nonCombatOnly) { final Game game = ai.getGame(); // a creature will [hopefully] die from a spell on stack boolean willDieFromSpell = false; boolean noStackCheck = false; - AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); - if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) { - // See if permission is on stack and ignore this check if there is and the relevant AI flag is set - // TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells. - for (SpellAbilityStackInstance si : game.getStack()) { - SpellAbility sa = si.getSpellAbility(); - if (sa.getApi() == ApiType.Counter) { - noStackCheck = true; - break; + if (ai.getController().isAI()) { + AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); + if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) { + // See if permission is on stack and ignore this check if there is and the relevant AI flag is set + // TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells. + for (SpellAbilityStackInstance si : game.getStack()) { + SpellAbility sa = si.getSpellAbility(); + if (sa.getApi() == ApiType.Counter) { + noStackCheck = true; + break; + } } } } willDieFromSpell = !noStackCheck && predictThreatenedObjects(creature.getController(), excludeSa).contains(creature); + if (nonCombatOnly) { + return willDieFromSpell; + } + // a creature will die as a result of combat boolean willDieInCombat = !willDieFromSpell && game.getPhaseHandler().inCombat() && ComputerUtilCombat.combatantWouldBeDestroyed(creature.getController(), creature, game.getCombat()); @@ -3257,5 +3308,20 @@ public class ComputerUtil { List list = c.getGame().getReplacementHandler().getReplacementList(ReplacementType.Moved, repParams, ReplacementLayer.CantHappen); return !list.isEmpty(); } - + + public static boolean shouldSacrificeThreatenedCard(Player ai, Card c, SpellAbility sa) { + if (!ai.getController().isAI()) { + return false; // only makes sense for actual AI decisions + } else if (sa != null && sa.getApi() == ApiType.Regenerate && sa.getHostCard().equals(c)) { + return false; // no use in sacrificing a card in an attempt to regenerate it + } + ComputerUtilCost.setSuppressRecursiveSacCostCheck(true); + Game game = ai.getGame(); + Combat combat = game.getCombat(); + boolean isThreatened = (c.isCreature() && ComputerUtil.predictCreatureWillDieThisTurn(ai, c, sa, false) + && (!ComputerUtilCombat.willOpposingCreatureDieInCombat(ai, c, combat) && !ComputerUtilCombat.isDangerousToSacInCombat(ai, c, combat))) + || (!c.isCreature() && ComputerUtil.predictThreatenedObjects(ai, sa).contains(c)); + ComputerUtilCost.setSuppressRecursiveSacCostCheck(false); + return isThreatened; + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index 1e53eee050b..7cbcacda85a 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -2583,4 +2583,43 @@ public class ComputerUtilCombat { } return totalLifeLinkDamage; } + + public static boolean willOpposingCreatureDieInCombat(final Player ai, final Card combatant, final Combat combat) { + if (combat != null) { + if (combat.isBlocking(combatant)) { + for (Card atk : combat.getAttackersBlockedBy(combatant)) { + if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, atk, combat)) { + return true; + } + } + } else if (combat.isBlocked(combatant)) { + for (Card blk : combat.getBlockers(combatant)) { + if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, blk, combat)) { + return true; + } + } + } + } + return false; + } + + public static boolean isDangerousToSacInCombat(final Player ai, final Card combatant, final Combat combat) { + if (combat != null) { + if (combat.isBlocking(combatant)) { + if (combatant.hasKeyword(Keyword.BANDING)) { + return true; + } + for (Card atk : combat.getAttackersBlockedBy(combatant)) { + if (atk.hasKeyword(Keyword.TRAMPLE)) { + return true; + } + } + } else if (combat.isBlocked(combatant)) { + if (combatant.hasKeyword(Keyword.BANDING)) { + return true; + } + } + } + return false; + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index 67a572a40fd..f7da6d2151c 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -35,6 +35,10 @@ import java.util.Set; public class ComputerUtilCost { + private static boolean suppressRecursiveSacCostCheck = false; + public static void setSuppressRecursiveSacCostCheck(boolean shouldSuppress) { + suppressRecursiveSacCostCheck = shouldSuppress; + } /** * Check add m1 m1 counter cost. @@ -344,6 +348,10 @@ public class ComputerUtilCost { } for (final CostPart part : cost.getCostParts()) { if (part instanceof CostSacrifice) { + if (suppressRecursiveSacCostCheck) { + return false; + } + final CostSacrifice sac = (CostSacrifice) part; final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility); @@ -358,11 +366,13 @@ public class ComputerUtilCost { } if (source.isCreature()) { // e.g. Sakura Tribe-Elder + final Combat combat = ai.getGame().getCombat(); final boolean beforeNextTurn = ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn().equals(ai); - final boolean creatureInDanger = ComputerUtil.predictCreatureWillDieThisTurn(ai, source, sourceAbility); - final int lifeThreshold = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD)); + final boolean creatureInDanger = ComputerUtil.predictCreatureWillDieThisTurn(ai, source, sourceAbility, false) + && !ComputerUtilCombat.willOpposingCreatureDieInCombat(ai, source, combat); + final int lifeThreshold = ai.getController().isAI() ? (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD)) : 4; final boolean aiInDanger = ai.getLife() <= lifeThreshold && ai.canLoseLife() && !ai.cantLoseForZeroOrLessLife(); - if (creatureInDanger) { + if (creatureInDanger && !ComputerUtilCombat.isDangerousToSacInCombat(ai, source, combat)) { return true; } else if (aiInDanger || !beforeNextTurn) { return false; diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index a7a9fda7947..09a06bf1122 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -1610,6 +1610,22 @@ public class SpecialCardAi { } } + // The One Ring + public static class TheOneRing { + public static boolean consider(final Player ai, final SpellAbility sa) { + if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) { + return true; + } + + AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); + int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD); + int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN); + + return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger + && ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1; + } + } + // The Scarab God public static class TheScarabGod { public static boolean consider(final Player ai, final SpellAbility sa) { diff --git a/forge-ai/src/main/java/forge/ai/ability/BranchAi.java b/forge-ai/src/main/java/forge/ai/ability/BranchAi.java index 0d63c224c88..1ba479e6faf 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BranchAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BranchAi.java @@ -1,13 +1,11 @@ package forge.ai.ability; -import java.util.Map; - import com.google.common.base.Predicate; -import forge.ai.ComputerUtilCard; -import forge.ai.SpecialCardAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.GameEntity; +import forge.game.ability.AbilityUtils; +import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; @@ -15,6 +13,9 @@ import forge.game.combat.Combat; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.spellability.SpellAbility; +import forge.util.Expressions; + +import java.util.Map; public class BranchAi extends SpellAbilityAi { /* (non-Javadoc) @@ -25,6 +26,37 @@ public class BranchAi extends SpellAbilityAi { final String aiLogic = sa.getParamOrDefault("AILogic", ""); if ("GrislySigil".equals(aiLogic)) { return SpecialCardAi.GrislySigil.consider(aiPlayer, sa); + } else if ("BranchCounter".equals(aiLogic)) { + // TODO: this might need expanding/tweaking if more cards are added with different SA setups + SpellAbility top = ComputerUtilAbility.getTopSpellAbilityOnStack(aiPlayer.getGame(), sa); + if (top == null || !sa.canTarget(top)) { + return false; + } + Card host = sa.getHostCard(); + + // pre-target the object to calculate the branch condition SVar, then clean up before running the real check + sa.getTargets().add(top); + int value = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("BranchConditionSVar"), sa); + sa.resetTargets(); + + String branchCompare = sa.getParamOrDefault("BranchConditionSVarCompare", "GE1"); + String operator = branchCompare.substring(0, 2); + String operand = branchCompare.substring(2); + final int operandValue = AbilityUtils.calculateAmount(host, operand, sa); + boolean conditionMet = Expressions.compare(value, operator, operandValue); + + SpellAbility falseSub = sa.getAdditionalAbility("FalseSubAbility"); // this ability has the UnlessCost part + boolean willPlay = false; + if (!conditionMet && falseSub.hasParam("UnlessCost")) { + // FIXME: We're emulating the UnlessCost on the SA to run the proper checks. + // This is hacky, but it works. Perhaps a cleaner way exists? + sa.getMapParams().put("UnlessCost", falseSub.getParam("UnlessCost")); + willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(aiPlayer, sa); + sa.getMapParams().remove("UnlessCost"); + } else { + willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(aiPlayer, sa); + } + return willPlay; } else if ("TgtAttacker".equals(aiLogic)) { final Combat combat = aiPlayer.getGame().getCombat(); if (combat == null || combat.getAttackingPlayer() != aiPlayer) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java index bbee876d935..efeec0063d3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -768,6 +768,8 @@ public class ChangeZoneAi extends SpellAbilityAi { if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) { return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN); + } else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) { + return true; } if (sa.isHidden()) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java index 010b8f0fd9c..93bb914454b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java @@ -44,6 +44,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi { final Game game = ai.getGame(); final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0); + final String aiLogic = sa.getParamOrDefault("AILogic" ,""); if (abCost != null) { // AI currently disabled for these costs @@ -52,7 +53,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi { } if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { - boolean aiLogicAllowsDiscard = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("DiscardAll"); + boolean aiLogicAllowsDiscard = aiLogic.startsWith("DiscardAll"); if (!aiLogicAllowsDiscard) { return false; @@ -86,16 +87,16 @@ public class ChangeZoneAllAi extends SpellAbilityAi { oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa); computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa); - if ("LivingDeath".equals(sa.getParam("AILogic"))) { + if ("LivingDeath".equals(aiLogic)) { // Living Death AI return SpecialCardAi.LivingDeath.consider(ai, sa); - } else if ("Timetwister".equals(sa.getParam("AILogic"))) { + } else if ("Timetwister".equals(aiLogic)) { // Timetwister AI return SpecialCardAi.Timetwister.consider(ai, sa); - } else if ("RetDiscardedThisTurn".equals(sa.getParam("AILogic"))) { + } else if ("RetDiscardedThisTurn".equals(aiLogic)) { // e.g. Shadow of the Grave return ai.getNumDiscardedThisTurn() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN); - } else if ("ExileGraveyards".equals(sa.getParam("AILogic"))) { + } else if ("ExileGraveyards".equals(aiLogic)) { for (Player opp : ai.getOpponents()) { CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard); CardCollection creats = CardLists.filter(cardsGY, CardPredicates.Presets.CREATURES); @@ -105,7 +106,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi { } } return false; - } else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) { + } else if ("ManifestCreatsFromGraveyard".equals(aiLogic)) { PlayerCollection players = ai.getOpponents(); players.add(ai); int maxSize = 1; @@ -217,7 +218,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi { } // Don't cast during main1? - if (game.getPhaseHandler().is(PhaseType.MAIN1, ai)) { + if (game.getPhaseHandler().is(PhaseType.MAIN1, ai) && !aiLogic.equals("Main1")) { return false; } } else if (origin.equals(ZoneType.Graveyard)) { @@ -245,15 +246,13 @@ public class ChangeZoneAllAi extends SpellAbilityAi { && !ComputerUtil.isPlayingReanimator(ai); } } else if (origin.equals(ZoneType.Exile)) { - String logic = sa.getParam("AILogic"); - - if (logic != null && logic.startsWith("DiscardAllAndRetExiled")) { + if (aiLogic.startsWith("DiscardAllAndRetExiled")) { int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size(); int curHandSize = ai.getCardsIn(ZoneType.Hand).size(); // minimum card advantage unless the hand will be fully reloaded - int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0; - boolean noDiscard = logic.contains(".noDiscard"); + int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0; + boolean noDiscard = aiLogic.contains(".noDiscard"); if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) { if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 39508102c23..1cff702ed3a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -308,8 +308,10 @@ public class CountersPutAi extends CountersAi { } else if (logic.equals("ChargeToBestCMC")) { return doChargeToCMCLogic(ai, sa); } else if (logic.equals("ChargeToBestOppControlledCMC")) { - return doChargeToOppCtrlCMCLogic(ai, sa); - } + return doChargeToOppCtrlCMCLogic(ai, sa); + } else if (logic.equals("TheOneRing")) { + return SpecialCardAi.TheOneRing.consider(ai, sa); + } if (!sa.metConditions() && sa.getSubAbility() == null) { return false; @@ -440,6 +442,9 @@ public class CountersPutAi extends CountersAi { } } + final boolean hasSacCost = abCost.hasSpecificCostType(CostSacrifice.class); + final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost); + if (sa.usesTargeting()) { if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) { // only evaluates case where all tokens are placed on a single target @@ -453,15 +458,15 @@ public class CountersPutAi extends CountersAi { sa.addDividedAllocation(c, amount); return true; } else { - return false; + if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies + return false; + } } } } sa.resetTargets(); - final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost); - if (sa.isCurse()) { list = ai.getOpponents().getCardsIn(ZoneType.Battlefield); } else { @@ -474,6 +479,8 @@ public class CountersPutAi extends CountersAi { // don't put the counter on the dead creature if (sacSelf && c.equals(source)) { return false; + } else if (hasSacCost && !ComputerUtil.shouldSacrificeThreatenedCard(ai, c, sa)) { + return false; } if ("NoCounterOfType".equals(sa.getParam("AILogic"))) { for (String ctrType : types) { @@ -628,7 +635,7 @@ public class CountersPutAi extends CountersAi { } // Instant +1/+1 if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) { - if (!(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) { + if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) { return false; // only if next turn and cost is reusable } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java index a813ce8f182..7b16ec65525 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java @@ -274,7 +274,8 @@ public class DamageAllAi extends SpellAbilityAi { final String damage = sa.getParam("NumDmg"); int dmg; - if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) { + if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") + && sa.getPayCosts() != null && sa.getPayCosts().hasXInAnyCostPart()) { // Set PayX here to maximum value. dmg = ComputerUtilCost.getMaxXValue(sa, ai, true); sa.setXManaCostPaid(dmg); diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index 9c4f48cfd5b..ddf543aab55 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -752,7 +752,7 @@ public class DamageDealAi extends DamageAiBase { int minTgts = tgt.getMinTargets(source, sa); if (tcs.size() < minTgts || tcs.size() == 0) { if (mandatory) { - // Sanity check: if there are any legal non-owned targets after the check (which may happen for complex cards like Searing Blaze), + // Sanity check: if there are any legal non-owned targets after the check (which may happen for complex cards like Rift Bolt), // choose a random opponent's target before forcing targeting of own stuff List allTgtEntities = sa.getTargetRestrictions().getAllCandidates(sa, true); for (GameEntity ent : allTgtEntities) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java index a1f0d31e9ec..c871935ae68 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -35,11 +35,7 @@ import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CounterEnumType; import forge.game.card.CounterType; -import forge.game.cost.Cost; -import forge.game.cost.CostDiscard; -import forge.game.cost.CostPart; -import forge.game.cost.CostPayLife; -import forge.game.cost.PaymentDecision; +import forge.game.cost.*; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -148,9 +144,15 @@ public class DrawAi extends SpellAbilityAi { return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard()); } + // Sacrificing a creature in response to something dangerous is generally good in any phase + boolean isSacCost = false; + if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { + isSacCost = true; + } + // Don't use draw abilities before main 2 if possible if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") - && !ComputerUtil.castSpellInMain1(ai, sa)) { + && !ComputerUtil.castSpellInMain1(ai, sa) && !isSacCost) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java index f287d3eb2a7..a1a93174e8d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -396,10 +396,17 @@ public class EffectAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { + if (sa.hasParam("AILogic")) { + if (canPlayAI(aiPlayer, sa)) { + return true; // if false, fall through further to do the mandatory stuff + } + } + // E.g. Nova Pentacle if (sa.usesTargeting() && !sa.getTargetRestrictions().canTgtPlayer()) { // try to target the opponent's best targetable permanent, if able CardCollection oppPerms = CardLists.getValidCards(aiPlayer.getOpponents().getCardsIn(sa.getTargetRestrictions().getZone()), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa); + oppPerms = CardLists.filter(oppPerms, card -> sa.canTarget(card)); if (!oppPerms.isEmpty()) { sa.resetTargets(); sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms)); @@ -409,6 +416,7 @@ public class EffectAi extends SpellAbilityAi { if (mandatory) { // try to target the AI's worst targetable permanent, if able CardCollection aiPerms = CardLists.getValidCards(aiPlayer.getCardsIn(sa.getTargetRestrictions().getZone()), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa); + aiPerms = CardLists.filter(aiPerms, card -> sa.canTarget(card)); if (!aiPerms.isEmpty()) { sa.resetTargets(); sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms)); diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java index 48f3585c596..1185df31edd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java @@ -81,6 +81,7 @@ public class LifeGainAi extends SpellAbilityAi { protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { final Game game = ai.getGame(); final int life = ai.getLife(); + final String aiLogic = sa.getParamOrDefault("AILogic", ""); boolean activateForCost = ComputerUtil.activateForCost(sa, ai); boolean lifeCritical = life <= 5; @@ -103,9 +104,15 @@ public class LifeGainAi extends SpellAbilityAi { if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return false; } } + // Sacrificing in response to something dangerous is generally good in any phase + boolean isSacCost = false; + if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { + isSacCost = true; + } + // Don't use lifegain before main 2 if possible if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") - && !ComputerUtil.castSpellInMain1(ai, sa)) { + && !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) { return false; } @@ -124,6 +131,7 @@ public class LifeGainAi extends SpellAbilityAi { protected boolean checkApiLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); + final String aiLogic = sa.getParamOrDefault("AILogic", ""); final int life = ai.getLife(); final String amountStr = sa.getParam("LifeAmount"); @@ -185,7 +193,11 @@ public class LifeGainAi extends SpellAbilityAi { || sa.getSubAbility() != null || playReusable(ai, sa)) { return true; } - + + if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { + return true; // sac costs should be performed at Instant speed when able + } + // Save instant-speed life-gain unless it is really worth it final float value = 0.9f * lifeAmount / life; if (value < 0.2f) { diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java index b7f75808bc5..b261a2fcf02 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java @@ -10,6 +10,7 @@ import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.cost.Cost; +import forge.game.cost.CostSacrifice; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerCollection; @@ -96,6 +97,7 @@ public class LifeLoseAi extends SpellAbilityAi { protected boolean checkApiLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String amountStr = sa.getParam("LifeAmount"); + final String aiLogic = sa.getParamOrDefault("AILogic", ""); int amount = 0; if (sa.usesTargeting()) { @@ -133,9 +135,15 @@ public class LifeLoseAi extends SpellAbilityAi { return true; } + // Sacrificing a creature in response to something dangerous is generally good in any phase + boolean isSacCost = false; + if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { + isSacCost = true; + } + // Don't use loselife before main 2 if possible if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") - && !ComputerUtil.castSpellInMain1(ai, sa) && !"AnyPhase".equals(sa.getParam("AILogic"))) { + && !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) { return false; } 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 371cbca3a1d..5525a2a539f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java @@ -6,14 +6,7 @@ import java.util.List; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; -import forge.ai.AiPlayDecision; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilAbility; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCost; -import forge.ai.ComputerUtilMana; -import forge.ai.PlayerControllerAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaAtom; @@ -50,7 +43,7 @@ public class ManaEffectAi extends SpellAbilityAi { */ @Override protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) { - if (aiLogic.startsWith("ManaRitual")) { + if (aiLogic.startsWith("ManaRitual") || aiLogic.startsWith("BlackLotus")) { return doManaRitualLogic(ai, sa, false); } else if ("Always".equals(aiLogic)) { return true; diff --git a/forge-gui/res/ai/Cautious.ai b/forge-gui/res/ai/Cautious.ai index de33a3395b7..9eb9b44bfc8 100644 --- a/forge-gui/res/ai/Cautious.ai +++ b/forge-gui/res/ai/Cautious.ai @@ -299,3 +299,16 @@ MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR=50 # attempt this either in its upkeep or its draw phase or main 1). MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT=15 +# Master toggle for the following options setting the default AIPreference:SacCost handling. +SACRIFICE_DEFAULT_PREF_ENABLE=false +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card if its mana value (CMC) matches the specified minimum +SACRIFICE_DEFAULT_PREF_MIN_CMC=0 +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card if its mana value (CMC) matches the specified maximum +SACRIFICE_DEFAULT_PREF_MAX_CMC=1 +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card is a token +SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true +# A creature should evaluate to no more than this much to be considered for default SacCost preference +SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 \ No newline at end of file diff --git a/forge-gui/res/ai/Default.ai b/forge-gui/res/ai/Default.ai index ef486a337dc..d4e1bcc8763 100644 --- a/forge-gui/res/ai/Default.ai +++ b/forge-gui/res/ai/Default.ai @@ -299,3 +299,17 @@ MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR=50 # The chance that the AI will activate Jhoira's copy random instant ability (per phase, the AI will generally # attempt this either in its upkeep or its draw phase or main 1). MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT=20 + +# Master toggle for the following options setting the default AIPreference:SacCost handling. +SACRIFICE_DEFAULT_PREF_ENABLE=false +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card if its mana value (CMC) matches the specified minimum +SACRIFICE_DEFAULT_PREF_MIN_CMC=0 +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card if its mana value (CMC) matches the specified maximum +SACRIFICE_DEFAULT_PREF_MAX_CMC=2 +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card is a token +SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true +# A creature should evaluate to no more than this much to be considered for default SacCost preference +SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 \ No newline at end of file diff --git a/forge-gui/res/ai/Experimental.ai b/forge-gui/res/ai/Experimental.ai index 151a5f8a009..3d05b03bf4f 100644 --- a/forge-gui/res/ai/Experimental.ai +++ b/forge-gui/res/ai/Experimental.ai @@ -300,8 +300,22 @@ MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR=50 # attempt this either in its upkeep or its draw phase or main 1). MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT=20 +# Master toggle for the following options setting the default AIPreference:SacCost handling. +SACRIFICE_DEFAULT_PREF_ENABLE=true +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card if its mana value (CMC) matches the specified minimum +SACRIFICE_DEFAULT_PREF_MIN_CMC=0 +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card if its mana value (CMC) matches the specified maximum +SACRIFICE_DEFAULT_PREF_MAX_CMC=1 +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card is a token +SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true +# A creature should evaluate to no more than this much to be considered for default SacCost preference +SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 + # -- Experimental feature toggles which only exist until the testing procedure for the relevant -- # -- features is over. These toggles will be removed later, or may be reintroduced under a -- # -- different name if necessary -- -# <-- there are no experimental options here at the moment --> +# <-- there are no experimental options here at the moment --> \ No newline at end of file diff --git a/forge-gui/res/ai/Reckless.ai b/forge-gui/res/ai/Reckless.ai index 58f44c24067..4d092ff8029 100644 --- a/forge-gui/res/ai/Reckless.ai +++ b/forge-gui/res/ai/Reckless.ai @@ -300,3 +300,16 @@ MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR=50 # attempt this either in its upkeep or its draw phase or main 1). MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT=20 +# Master toggle for the following options setting the default AIPreference:SacCost handling. +SACRIFICE_DEFAULT_PREF_ENABLE=false +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card if its mana value (CMC) matches the specified minimum +SACRIFICE_DEFAULT_PREF_MIN_CMC=0 +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card if its mana value (CMC) matches the specified maximum +SACRIFICE_DEFAULT_PREF_MAX_CMC=3 +# If a card has no card-specific AIPreference for the Sacrifice cost (AIPreference:SacCost), the AI will still +# consider the sacrifice of a matching card is a token +SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS=true +# A creature should evaluate to no more than this much to be considered for default SacCost preference +SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL=135 \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/a/animal_boneyard.txt b/forge-gui/res/cardsfolder/a/animal_boneyard.txt index f4181417a46..894147a169e 100644 --- a/forge-gui/res/cardsfolder/a/animal_boneyard.txt +++ b/forge-gui/res/cardsfolder/a/animal_boneyard.txt @@ -6,6 +6,5 @@ A:SP$ Attach | Cost$ 2 W | ValidTgts$ Land | AILogic$ Pump S:Mode$ Continuous | Affected$ Land.AttachedBy | AddAbility$ GainLife | AddSVar$ AnimalBoneyardX | Description$ Enchanted land has "{T}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness." SVar:GainLife:AB$ GainLife | Cost$ T Sac<1/Creature> | LifeAmount$ AnimalBoneyardX | SpellDescription$ You gain life equal to the sacrificed creature's toughness. SVar:AnimalBoneyardX:Sacrificed$CardToughness -AI:RemoveDeck:All SVar:NonStackingAttachEffect:True Oracle:Enchant land\nEnchanted land has "{T}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness." diff --git a/forge-gui/res/cardsfolder/a/anticognition.txt b/forge-gui/res/cardsfolder/a/anticognition.txt index e5db2eda72c..8a6b95181c2 100644 --- a/forge-gui/res/cardsfolder/a/anticognition.txt +++ b/forge-gui/res/cardsfolder/a/anticognition.txt @@ -1,8 +1,7 @@ Name:Anticognition ManaCost:1 U Types:Instant -A:SP$ Pump | Cost$ 1 U | IsCurse$ True | TargetType$ Spell | TgtZone$ Stack | TgtPrompt$ Select target creature or planeswalker spell | ValidTgts$ Creature,Planeswalker | SubAbility$ DBBranch | StackDescription$ SpellDescription | SpellDescription$ Counter target creature or planeswalker spell unless its controller pays {2}. If an opponent has eight or more cards in their graveyard, instead counter that spell, then scry 2. -SVar:DBBranch:DB$ Branch | BranchConditionSVar$ X | BranchConditionSVarCompare$ GE8 | TrueSubAbility$ CounterScry | FalseSubAbility$ CounterUnless | StackDescription$ None +A:SP$ Branch | BranchConditionSVar$ X | TargetType$ Spell | TgtZone$ Stack | ValidTgts$ Creature,Planeswalker | BranchConditionSVarCompare$ GE8 | TrueSubAbility$ CounterScry | FalseSubAbility$ CounterUnless | AILogic$ BranchCounter | SpellDescription$ Counter target creature or planeswalker spell unless its controller pays {2}. If an opponent has eight or more cards in their graveyard, instead counter that spell, then scry 2. SVar:CounterUnless:DB$ Counter | Defined$ Targeted | UnlessCost$ 2 SVar:CounterScry:DB$ Counter | Defined$ Targeted | SubAbility$ DBScry SVar:DBScry:DB$ Scry | ScryNum$ 2 diff --git a/forge-gui/res/cardsfolder/a/arguels_blood_fast_temple_of_aclazotz.txt b/forge-gui/res/cardsfolder/a/arguels_blood_fast_temple_of_aclazotz.txt index c5176d6c30d..a32552eaf34 100644 --- a/forge-gui/res/cardsfolder/a/arguels_blood_fast_temple_of_aclazotz.txt +++ b/forge-gui/res/cardsfolder/a/arguels_blood_fast_temple_of_aclazotz.txt @@ -4,7 +4,7 @@ Types:Legendary Enchantment A:AB$ Draw | Cost$ 1 B PayLife<2> | SpellDescription$ Draw a card. T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ DBTransform | LifeTotal$ You | LifeAmount$ LE5 | TriggerDescription$ At the beginning of your upkeep, if you have 5 or less life, you may transform CARDNAME. SVar:DBTransform:DB$ SetState | Defined$ Self | Mode$ Transform -AI:RemoveDeck:All +AI:RemoveDeck:Random AlternateMode:DoubleFaced Oracle:{1}{B}, Pay 2 life: Draw a card.\nAt the beginning of your upkeep, if you have 5 or less life, you may transform Arguel's Blood Fast. diff --git a/forge-gui/res/cardsfolder/a/atog.txt b/forge-gui/res/cardsfolder/a/atog.txt index 7ee84fab0ae..3aa552e3659 100644 --- a/forge-gui/res/cardsfolder/a/atog.txt +++ b/forge-gui/res/cardsfolder/a/atog.txt @@ -5,4 +5,5 @@ PT:1/2 A:AB$ Pump | Cost$ Sac<1/Artifact> | Defined$ Self | NumAtt$ 2 | NumDef$ 2 | SpellDescription$ CARDNAME gets +2/+2 until end of turn. DeckNeeds:Type$Artifact DeckHas:Ability$Sacrifice +SVar:AIPreference:SacCost$Artifact.token,Artifact.cmcEQ0+nonLegendary+notnamedBlack Lotus,Artifact.cmcEQ1,Artifact.cmcEQ2,Artifact.cmcEQ3 Oracle:Sacrifice an artifact: Atog gets +2/+2 until end of turn. diff --git a/forge-gui/res/cardsfolder/b/bone_splinters.txt b/forge-gui/res/cardsfolder/b/bone_splinters.txt index 847702dd61b..d2e9d99c479 100644 --- a/forge-gui/res/cardsfolder/b/bone_splinters.txt +++ b/forge-gui/res/cardsfolder/b/bone_splinters.txt @@ -2,6 +2,6 @@ Name:Bone Splinters ManaCost:B Types:Sorcery A:SP$ Destroy | Cost$ B Sac<1/Creature> | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Destroy target creature. -SVar:AICostPreference:SacCost$Creature.Token,Creature.cmcLE2 +SVar:AIPreference:SacCost$Creature.Token,Creature.cmcLE2 AI:RemoveDeck:Random Oracle:As an additional cost to cast this spell, sacrifice a creature.\nDestroy target creature. diff --git a/forge-gui/res/cardsfolder/b/bring_the_ending.txt b/forge-gui/res/cardsfolder/b/bring_the_ending.txt index 234b39e2abf..b1ea506c5d4 100644 --- a/forge-gui/res/cardsfolder/b/bring_the_ending.txt +++ b/forge-gui/res/cardsfolder/b/bring_the_ending.txt @@ -1,7 +1,7 @@ Name:Bring the Ending ManaCost:1 U Types:Instant -A:SP$ Branch | BranchConditionSVar$ X | TargetType$ Spell | TgtZone$ Stack | ValidTgts$ Card | BranchConditionSVarCompare$ GE3 | TrueSubAbility$ Counter | FalseSubAbility$ CounterUnless | SpellDescription$ Counter target spell unless its controller pays {2}. Corrupted — Counter that spell instead if its controller has three or more poison counters. +A:SP$ Branch | BranchConditionSVar$ X | TargetType$ Spell | TgtZone$ Stack | ValidTgts$ Card | BranchConditionSVarCompare$ GE3 | TrueSubAbility$ Counter | FalseSubAbility$ CounterUnless | AILogic$ BranchCounter | SpellDescription$ Counter target spell unless its controller pays {2}. Corrupted — Counter that spell instead if its controller has three or more poison counters. SVar:CounterUnless:DB$ Counter | Defined$ Targeted | UnlessCost$ 2 SVar:Counter:DB$ Counter | Defined$ Targeted SVar:X:TargetedController$PoisonCounters diff --git a/forge-gui/res/cardsfolder/c/corrupted_harvester.txt b/forge-gui/res/cardsfolder/c/corrupted_harvester.txt index 1a66fd1127f..528bd599e02 100644 --- a/forge-gui/res/cardsfolder/c/corrupted_harvester.txt +++ b/forge-gui/res/cardsfolder/c/corrupted_harvester.txt @@ -3,6 +3,6 @@ ManaCost:4 B B Types:Creature Phyrexian Horror PT:6/3 A:AB$ Regenerate | Cost$ B Sac<1/Creature> | SpellDescription$ Regenerate CARDNAME. -SVar:AIPreferences:SacCost$Creature.token,Creature.cmcLE5+powerLE3+toughnessLE4 +SVar:AIPreference:SacCost$Creature.token,Creature.cmcLE5+powerLE3+toughnessLE4 AI:RemoveDeck:Random Oracle:{B}, Sacrifice a creature: Regenerate Corrupted Harvester. diff --git a/forge-gui/res/cardsfolder/c/cryptbreaker.txt b/forge-gui/res/cardsfolder/c/cryptbreaker.txt index 5e5c707fb70..b2bc3706646 100644 --- a/forge-gui/res/cardsfolder/c/cryptbreaker.txt +++ b/forge-gui/res/cardsfolder/c/cryptbreaker.txt @@ -3,7 +3,7 @@ ManaCost:B Types:Creature Zombie PT:1/1 A:AB$ Token | Cost$ 1 B T Discard<1/Card> | TokenAmount$ 1 | TokenScript$ b_2_2_zombie | TokenOwner$ You | SpellDescription$ Create a 2/2 black Zombie creature token. -A:AB$ Draw | Cost$ tapXType<3/Zombie> | NumCards$ 1 | SpellDescription$ You draw a card and you lose 1 life. | SubAbility$ DBLoseLife +A:AB$ Draw | Cost$ tapXType<3/Zombie> | NumCards$ 1 | AILogic$ AtOppEOT | SpellDescription$ You draw a card and you lose 1 life. | SubAbility$ DBLoseLife SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 1 SVar:AIPreference:DiscardCost$Card DeckNeeds:Type$Zombie diff --git a/forge-gui/res/cardsfolder/d/diamond_valley.txt b/forge-gui/res/cardsfolder/d/diamond_valley.txt index 3b907b40ccc..038681e827f 100644 --- a/forge-gui/res/cardsfolder/d/diamond_valley.txt +++ b/forge-gui/res/cardsfolder/d/diamond_valley.txt @@ -4,5 +4,4 @@ Types:Land A:AB$ GainLife | Cost$ T Sac<1/Creature> | LifeAmount$ X | SpellDescription$ You gain life equal to the sacrificed creature's toughness. SVar:X:Sacrificed$CardToughness DeckHas:Ability$Sacrifice -AI:RemoveDeck:All Oracle:{T}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness. diff --git a/forge-gui/res/cardsfolder/d/disciple_of_griselbrand.txt b/forge-gui/res/cardsfolder/d/disciple_of_griselbrand.txt index 423c033af85..6c22552173b 100644 --- a/forge-gui/res/cardsfolder/d/disciple_of_griselbrand.txt +++ b/forge-gui/res/cardsfolder/d/disciple_of_griselbrand.txt @@ -4,5 +4,4 @@ Types:Creature Human Cleric PT:1/1 A:AB$ GainLife | Cost$ 1 Sac<1/Creature> | Defined$ You | LifeAmount$ X | SpellDescription$ You gain life equal to the sacrificed creature's toughness. SVar:X:Sacrificed$CardToughness -AI:RemoveDeck:All Oracle:{1}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness. diff --git a/forge-gui/res/cardsfolder/h/hallowed_moonlight.txt b/forge-gui/res/cardsfolder/h/hallowed_moonlight.txt index 08c56683ff1..c4fb3ae558a 100644 --- a/forge-gui/res/cardsfolder/h/hallowed_moonlight.txt +++ b/forge-gui/res/cardsfolder/h/hallowed_moonlight.txt @@ -1,9 +1,8 @@ Name:Hallowed Moonlight ManaCost:1 W Types:Instant -A:SP$ Effect | Cost$ 1 W | ReplacementEffects$ ReplaceExile | SubAbility$ DBDraw | SpellDescription$ Until end of turn, if a creature would enter the battlefield and it wasn't cast, exile it instead. Draw a card. +A:SP$ Effect | Cost$ 1 W | ReplacementEffects$ ReplaceExile | SubAbility$ DBDraw | AILogic$ NonCastCreature | SpellDescription$ Until end of turn, if a creature would enter the battlefield and it wasn't cast, exile it instead. Draw a card. SVar:ReplaceExile:Event$ Moved | ActiveZones$ Command | Destination$ Battlefield | ValidCard$ Creature.wasNotCast | ReplaceWith$ DBExile | Description$ If a creature would enter the battlefield and it wasn't cast, exile it instead. SVar:DBExile:DB$ ChangeZone | Hidden$ True | Origin$ All | Destination$ Exile | Defined$ ReplacedCard SVar:DBDraw:DB$ Draw | NumCards$ 1 -AI:RemoveDeck:All Oracle:Until end of turn, if a creature would enter the battlefield and it wasn't cast, exile it instead.\nDraw a card. diff --git a/forge-gui/res/cardsfolder/h/high_market.txt b/forge-gui/res/cardsfolder/h/high_market.txt index 525d2f47d96..d2e8222b76f 100644 --- a/forge-gui/res/cardsfolder/h/high_market.txt +++ b/forge-gui/res/cardsfolder/h/high_market.txt @@ -3,5 +3,4 @@ ManaCost:no cost Types:Land A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}. A:AB$ GainLife | Cost$ T Sac<1/Creature> | LifeAmount$ 1 | SpellDescription$ You gain 1 life. -AI:RemoveDeck:All Oracle:{T}: Add {C}.\n{T}, Sacrifice a creature: You gain 1 life. diff --git a/forge-gui/res/cardsfolder/h/hurler_cyclops.txt b/forge-gui/res/cardsfolder/h/hurler_cyclops.txt index dc1dc942d8a..f890cd4207b 100644 --- a/forge-gui/res/cardsfolder/h/hurler_cyclops.txt +++ b/forge-gui/res/cardsfolder/h/hurler_cyclops.txt @@ -3,6 +3,6 @@ ManaCost:3 R R Types:Creature Cyclops PT:5/4 A:AB$ DealDamage | Cost$ 1 Sac<1/Creature.Other/another creature> | ValidTgts$ Any | NumDmg$ 1 | SpellDescription$ CARDNAME deals 1 damage to any target. -SVar:AICostPreference:SacCost$Creature.Token,Creature.cmcLE2 +SVar:AIPreference:SacCost$Creature.Token,Creature.cmcLE2 AI:RemoveDeck:Random Oracle:{1}, Sacrifice another creature: Hurler Cyclops deals 1 damage to any target. \ No newline at end of file diff --git a/forge-gui/res/cardsfolder/k/kheru_dreadmaw.txt b/forge-gui/res/cardsfolder/k/kheru_dreadmaw.txt index b312ecf2573..e8a69a65704 100644 --- a/forge-gui/res/cardsfolder/k/kheru_dreadmaw.txt +++ b/forge-gui/res/cardsfolder/k/kheru_dreadmaw.txt @@ -6,5 +6,4 @@ K:Defender A:AB$ GainLife | Cost$ 1 G Sac<1/Creature.Other/another creature> | LifeAmount$ X | SubAbility$ DBCleanup | SpellDescription$ You gain life equal to the sacrificed creature's toughness. SVar:X:Sacrificed$CardToughness SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True -AI:RemoveDeck:All Oracle:Defender\n{1}{G}, Sacrifice another creature: You gain life equal to the sacrificed creature's toughness. diff --git a/forge-gui/res/cardsfolder/m/miren_the_moaning_well.txt b/forge-gui/res/cardsfolder/m/miren_the_moaning_well.txt index d27f01c9072..950046b6bdd 100644 --- a/forge-gui/res/cardsfolder/m/miren_the_moaning_well.txt +++ b/forge-gui/res/cardsfolder/m/miren_the_moaning_well.txt @@ -4,5 +4,4 @@ Types:Legendary Land A:AB$ Mana | Cost$ T | Produced$ C | SpellDescription$ Add {C}. A:AB$ GainLife | Cost$ 3 T Sac<1/Creature> | LifeAmount$ X | SpellDescription$ You gain life equal to the sacrificed creature's toughness. SVar:X:Sacrificed$CardToughness -AI:RemoveDeck:All Oracle:{T}: Add {C}.\n{3}, {T}, Sacrifice a creature: You gain life equal to the sacrificed creature's toughness. diff --git a/forge-gui/res/cardsfolder/m/mutual_destruction.txt b/forge-gui/res/cardsfolder/m/mutual_destruction.txt index 0ce98b47e83..b45cb8fbe98 100644 --- a/forge-gui/res/cardsfolder/m/mutual_destruction.txt +++ b/forge-gui/res/cardsfolder/m/mutual_destruction.txt @@ -3,5 +3,5 @@ ManaCost:B Types:Sorcery S:Mode$ Continuous | CharacteristicDefining$ True | AddKeyword$ Flash | IsPresent$ Permanent.YouCtrl+hasKeywordFlash | Description$ CARDNAME has flash as long as you control a permanent with flash. A:SP$ Destroy | Cost$ B Sac<1/Creature> | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Destroy target creature. -SVar:AICostPreference:SacCost$Creature.Token,Creature.cmcLE2 +SVar:AIPreference:SacCost$Creature.Token,Creature.cmcLE2 Oracle:This spell has flash as long as you control a permanent with flash.\nAs an additional cost to cast this spell, sacrifice a creature.\nDestroy target creature. diff --git a/forge-gui/res/cardsfolder/rebalanced/a-the_one_ring.txt b/forge-gui/res/cardsfolder/rebalanced/a-the_one_ring.txt index 85341ba72f1..3e04f770ec1 100644 --- a/forge-gui/res/cardsfolder/rebalanced/a-the_one_ring.txt +++ b/forge-gui/res/cardsfolder/rebalanced/a-the_one_ring.txt @@ -6,7 +6,7 @@ T:Mode$ ChangesZone | ValidCard$ Card.wasCastByYou+Self | Destination$ Battlefie SVar:TrigPump:DB$ Pump | Defined$ You | Duration$ UntilYourNextTurn | KW$ Protection from everything T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigLoseLife | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, you lose 1 life for each burden counter on CARDNAME. SVar:TrigLoseLife:DB$ LoseLife | LifeAmount$ X -A:AB$ PutCounter | Cost$ 1 T | Defined$ Self | CounterType$ BURDEN | CounterNum$ 1 | SubAbility$ DBDraw | SpellDescription$ Put a burden counter on CARDNAME, then draw a card for each burden counter on CARDNAME. +A:AB$ PutCounter | Cost$ 1 T | Defined$ Self | CounterType$ BURDEN | CounterNum$ 1 | SubAbility$ DBDraw | AILogic$ TheOneRing | SpellDescription$ Put a burden counter on CARDNAME, then draw a card for each burden counter on CARDNAME. SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ X SVar:X:Count$CardCounters.BURDEN Oracle:Indestructible\nWhen The One Ring enters the battlefield, if you cast it, you gain protection from everything until your next turn.\nAt the beginning of your upkeep, you lose 1 life for each burden counter on The One Ring.\n{1}, {T}: Put a burden counter on The One Ring, then draw a card for each burden counter on The One Ring. diff --git a/forge-gui/res/cardsfolder/s/stoneforge_mystic.txt b/forge-gui/res/cardsfolder/s/stoneforge_mystic.txt index 42a59eae6c1..75e19209ad3 100644 --- a/forge-gui/res/cardsfolder/s/stoneforge_mystic.txt +++ b/forge-gui/res/cardsfolder/s/stoneforge_mystic.txt @@ -4,5 +4,5 @@ Types:Creature Kor Artificer PT:1/2 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChange | OptionalDecider$ You | TriggerDescription$ When CARDNAME enters the battlefield, you may search your library for an Equipment card, reveal it, put it into your hand, then shuffle. SVar:TrigChange:DB$ ChangeZone | Origin$ Library | Destination$ Hand | ChangeType$ Card.Equipment | ChangeNum$ 1 | ShuffleNonMandatory$ True -A:AB$ ChangeZone | Cost$ 1 W T | Origin$ Hand | Destination$ Battlefield | ChangeType$ Equipment | ChangeNum$ 1 | SpellDescription$ You may put an Equipment card from your hand onto the battlefield. +A:AB$ ChangeZone | Cost$ 1 W T | Origin$ Hand | Destination$ Battlefield | ChangeType$ Equipment | ChangeNum$ 1 | AILogic$ Main1 | SpellDescription$ You may put an Equipment card from your hand onto the battlefield. Oracle:When Stoneforge Mystic enters the battlefield, you may search your library for an Equipment card, reveal it, put it into your hand, then shuffle.\n{1}{W}, {T}: You may put an Equipment card from your hand onto the battlefield. diff --git a/forge-gui/res/cardsfolder/t/the_one_ring.txt b/forge-gui/res/cardsfolder/t/the_one_ring.txt index 472b79753b7..7355df96f6d 100644 --- a/forge-gui/res/cardsfolder/t/the_one_ring.txt +++ b/forge-gui/res/cardsfolder/t/the_one_ring.txt @@ -6,7 +6,7 @@ T:Mode$ ChangesZone | ValidCard$ Card.wasCastByYou+Self | Destination$ Battlefie SVar:TrigPump:DB$ Pump | Defined$ You | Duration$ UntilYourNextTurn | KW$ Protection from everything T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigLoseLife | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, you lose 1 life for each burden counter on CARDNAME. SVar:TrigLoseLife:DB$ LoseLife | LifeAmount$ X -A:AB$ PutCounter | Cost$ T | Defined$ Self | CounterType$ BURDEN | CounterNum$ 1 | SubAbility$ DBDraw | SpellDescription$ Put a burden counter on CARDNAME, then draw a card for each burden counter on CARDNAME. +A:AB$ PutCounter | Cost$ T | Defined$ Self | CounterType$ BURDEN | CounterNum$ 1 | SubAbility$ DBDraw | AILogic$ TheOneRing | SpellDescription$ Put a burden counter on CARDNAME, then draw a card for each burden counter on CARDNAME. SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ X SVar:X:Count$CardCounters.BURDEN Oracle:Indestructible\nWhen The One Ring enters the battlefield, if you cast it, you gain protection from everything until your next turn.\nAt the beginning of your upkeep, you lose 1 life for each burden counter on The One Ring.\n{T}: Put a burden counter on The One Ring, then draw a card for each burden counter on The One Ring. diff --git a/forge-gui/res/cardsfolder/v/vampiric_rites.txt b/forge-gui/res/cardsfolder/v/vampiric_rites.txt index bce9c0ce625..b487ee3a9e4 100644 --- a/forge-gui/res/cardsfolder/v/vampiric_rites.txt +++ b/forge-gui/res/cardsfolder/v/vampiric_rites.txt @@ -4,5 +4,4 @@ Types:Enchantment A:AB$ GainLife | Cost$ 1 B Sac<1/Creature> | Defined$ You | LifeAmount$ 1 | SubAbility$ DBDraw | SpellDescription$ You gain 1 life and draw a card. SVar:DBDraw:DB$ Draw | NumCards$ 1 SVar:NonStackingEffect:True -AI:RemoveDeck:All Oracle:{1}{B}, Sacrifice a creature: You gain 1 life and draw a card. diff --git a/forge-gui/res/cardsfolder/w/wall_of_limbs.txt b/forge-gui/res/cardsfolder/w/wall_of_limbs.txt index 2187645e1f3..641d2becd0f 100644 --- a/forge-gui/res/cardsfolder/w/wall_of_limbs.txt +++ b/forge-gui/res/cardsfolder/w/wall_of_limbs.txt @@ -7,6 +7,6 @@ T:Mode$ LifeGained | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ Tri SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1 A:AB$ LoseLife | Cost$ 5 B B Sac<1/CARDNAME> | LifeAmount$ X | ValidTgts$ Player | TgtPrompt$ Select a player | SpellDescription$ Target player loses X life, where X is CARDNAME's power. SVar:X:Sacrificed$CardPower -AI:RemoveDeck:All AI:RemoveDeck:Random +AI:RemoveDeck:All Oracle:Defender (This creature can't attack.)\nWhenever you gain life, put a +1/+1 counter on Wall of Limbs.\n{5}{B}{B}, Sacrifice Wall of Limbs: Target player loses X life, where X is Wall of Limbs's power. diff --git a/forge-gui/res/cardsfolder/w/winds_of_abandon.txt b/forge-gui/res/cardsfolder/w/winds_of_abandon.txt index 9fcc7e3b094..da99b81c127 100644 --- a/forge-gui/res/cardsfolder/w/winds_of_abandon.txt +++ b/forge-gui/res/cardsfolder/w/winds_of_abandon.txt @@ -2,7 +2,7 @@ Name:Winds of Abandon ManaCost:1 W Types:Sorcery A:SP$ ChangeZone | Cost$ 1 W | Origin$ Battlefield | Destination$ Exile | ValidTgts$ Creature.YouDontCtrl | TgtPrompt$ Select target creature you don't control | SubAbility$ DBGetLandsAll | RememberLKI$ True | SpellDescription$ Exile target creature you don't control. For each creature exiled this way, its controller searches their library for a basic land card. Those players put those cards onto the battlefield tapped, then shuffle. -A:SP$ ChangeZoneAll | Cost$ 4 W W | ChangeType$ Creature.YouDontCtrl | Origin$ Battlefield | Destination$ Exile | RememberLKI$ True | SubAbility$ DBGetLandsAll | PrecostDesc$ Overload | CostDesc$ {4}{W}{W} | NonBasicSpell$ True | SpellDescription$ (You may cast this spell for its overload cost. If you do, change its text by replacing all instances of "target" with "each.") +A:SP$ ChangeZoneAll | Cost$ 4 W W | ChangeType$ Creature.YouDontCtrl | Origin$ Battlefield | Destination$ Exile | RememberLKI$ True | SubAbility$ DBGetLandsAll | PrecostDesc$ Overload | CostDesc$ {4}{W}{W} | NonBasicSpell$ True | AILogic$ Main1 | SpellDescription$ (You may cast this spell for its overload cost. If you do, change its text by replacing all instances of "target" with "each.") SVar:DBGetLandsAll:DB$ RepeatEach | RepeatPlayers$ Player | RepeatSubAbility$ DBGetLandsOne | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:DBGetLandsOne:DB$ ChangeZone | Optional$ True | Origin$ Library | Destination$ Battlefield | Tapped$ True | ChangeType$ Land.Basic | ChangeNum$ X | DefinedPlayer$ Player.IsRemembered | ShuffleNonMandatory$ False | ConditionCheckSVar$ X | ConditionSVarCompare$ GE1