diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index edd2e6ed4a7..a14c00c48ea 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -775,9 +775,15 @@ public class AiController { if (currentState != null) { host.setState(sa.getCardStateName(), false); } + if (sa.isSpell()) { + host.setCastSA(sa); + } AiPlayDecision decision = canPlayAndPayForFace(sa); + if (sa.isSpell()) { + host.setCastSA(null); + } if (currentState != null) { host.setState(currentState, false); } @@ -918,7 +924,7 @@ public class AiController { Sentry.setExtra("Card", card.getName()); Sentry.setExtra("SA", sa.toString()); - boolean canPlay = SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(player, sa); + boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa); // remove added extra Sentry.removeExtra("Card"); @@ -1296,9 +1302,9 @@ public class AiController { if (spell instanceof SpellApiBased) { boolean chance = false; if (withoutPayingManaCost) { - chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerNoCostWithSubs(player, spell, mandatory); + chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory); } else { - chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory); + chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory); } if (!chance) { return AiPlayDecision.TargetingFailed; @@ -1620,38 +1626,45 @@ public class AiController { continue; } - if (sa.getHostCard().hasKeyword(Keyword.STORM) - && sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell - && player.getZone(ZoneType.Hand).contains( - Predicate.not(CardPredicates.LANDS.or(CardPredicates.hasKeyword("Storm"))) - )) { - if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) { - // skip evaluating Storm unless we reached the minimum Storm count - continue; - } - } - // living end AI decks - // TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed - AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa; - if (useLivingEnd) { - if (sa.isCycling() && sa.canCastTiming(player) && player.getCardsIn(ZoneType.Library).size() >= 10) { - if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) { - if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class) - && !player.cantLoseForZeroOrLessLife() - && player.getLife() <= sa.getPayCosts().getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) { - aiPlayDecision = AiPlayDecision.CantAfford; - } else { - aiPlayDecision = AiPlayDecision.WillPlay; - } + if (sa.getHostCard().hasKeyword(Keyword.STORM) + && sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell + && player.getZone(ZoneType.Hand).contains( + Predicate.not(CardPredicates.LANDS.or(CardPredicates.hasKeyword("Storm"))) + )) { + if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) { + // skip evaluating Storm unless we reached the minimum Storm count + continue; } - } else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) { - if (isLifeInDanger) { //needs more tune up for certain conditions - aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa : AiPlayDecision.WillPlay; - } else if (CardLists.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES).size() > 4) { + } + + // living end AI decks + // TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed + AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa; + if (useLivingEnd) { + if (sa.isCycling() && sa.canCastTiming(player) + && player.getCardsIn(ZoneType.Library).size() >= 10) { + if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) { + if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class) + && !player.cantLoseForZeroOrLessLife() && player.getLife() <= sa.getPayCosts() + .getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) { + aiPlayDecision = AiPlayDecision.CantAfford; + } else { + aiPlayDecision = AiPlayDecision.WillPlay; + } + } + } else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) { + if (isLifeInDanger) { // needs more tune up for certain conditions + aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa + : AiPlayDecision.WillPlay; + } else if (CardLists + .filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES) + .size() > 4) { if (player.getCreaturesInPlay().size() >= 4) // it's good minimum continue; - else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player) && ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) - aiPlayDecision = AiPlayDecision.WillPlay;// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard + else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player) + && ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) + aiPlayDecision = AiPlayDecision.WillPlay; + // needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard } else { continue; } @@ -1726,7 +1739,7 @@ public class AiController { if (spell instanceof WrappedAbility) return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory); if (spell.getApi() != null) - return SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory); + return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory); if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) { // For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about return true; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 266ab2307a2..bf8ce1a0ffa 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -906,7 +906,7 @@ public class ComputerUtil { // Run non-mandatory trigger. // These checks only work if the Executing SpellAbility is an Ability_Sub. - if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA.getApi()).doTriggerAI(ai, exSA, false)) { + if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) { // AI would not run this trigger if given the chance return sacrificed; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index e217d3040d0..19c4f9abc8d 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -21,6 +21,7 @@ import forge.game.cost.CostRemoveCounter; import forge.game.keyword.Keyword; import forge.game.phase.PhaseType; import forge.game.player.Player; +import forge.game.spellability.OptionalCost; import forge.game.spellability.OptionalCostValue; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; @@ -122,6 +123,10 @@ public class ComputerUtilAbility { boolean choseOptCost = false; List list = GameActionUtil.getOptionalCostValues(sa); if (!list.isEmpty()) { + // still add base spell in case of Promise Gift + if (list.stream().anyMatch(ocv -> ocv.getType().equals(OptionalCost.PromiseGift))) { + result.add(sa); + } list = player.getController().chooseOptionalCosts(sa, list); if (!list.isEmpty()) { choseOptCost = true; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 4aa79c74a8d..98138e091ff 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -1491,7 +1491,7 @@ public class ComputerUtilMana { AbilitySub sub = m.getSubAbility(); // We really shouldn't be hardcoding names here. ChkDrawback should just return true for them if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) { - if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) { + if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) { continue; } needsLimitedResources = true; // TODO: check for good drawbacks (gainLife) @@ -1571,7 +1571,7 @@ public class ComputerUtilMana { // don't use abilities with dangerous drawbacks AbilitySub sub = m.getSubAbility(); if (sub != null) { - if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) { + if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) { continue; } } diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 5f17455dfa5..3f6297e134a 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -46,7 +46,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; -import java.security.InvalidParameterException; import java.util.*; import java.util.function.Predicate; @@ -352,11 +351,7 @@ public class PlayerControllerAi extends PlayerController { if (delayedReveal != null) { reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix()); } - ApiType api = sa.getApi(); - if (null == api) { - throw new InvalidParameterException("SA is not api-based, this is not supported yet"); - } - return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection)optionList, isOptional, targetedPlayer, params); + return SpellApiToAi.Converter.get(sa).chooseSingleEntity(player, sa, (FCollection)optionList, isOptional, targetedPlayer, params); } @Override @@ -398,11 +393,7 @@ public class PlayerControllerAi extends PlayerController { @Override public SpellAbility chooseSingleSpellForEffect(List spells, SpellAbility sa, String title, Map params) { - ApiType api = sa.getApi(); - if (null == api) { - throw new InvalidParameterException("SA is not api-based, this is not supported yet"); - } - return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells, params); + return SpellApiToAi.Converter.get(sa).chooseSingleSpellAbility(player, sa, spells, params); } @Override @@ -876,11 +867,7 @@ public class PlayerControllerAi extends PlayerController { @Override public int chooseNumber(SpellAbility sa, String string, int min, int max, Map params) { - ApiType api = sa.getApi(); - if (null == api) { - throw new InvalidParameterException("SA is not api-based, this is not supported yet"); - } - return SpellApiToAi.Converter.get(api).chooseNumber(player, sa, min, max, params); + return SpellApiToAi.Converter.get(sa).chooseNumber(player, sa, min, max, params); } @Override @@ -982,11 +969,7 @@ public class PlayerControllerAi extends PlayerController { */ @Override public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Map params) { - ApiType api = sa.getApi(); - if (null == api) { - throw new InvalidParameterException("SA is not api-based, this is not supported yet"); - } - return SpellApiToAi.Converter.get(api).chooseBinary(kindOfChoice, sa, params); + return SpellApiToAi.Converter.get(sa).chooseBinary(kindOfChoice, sa, params); } @Override @@ -1056,11 +1039,7 @@ public class PlayerControllerAi extends PlayerController { if (options.size() <= 1) { return Iterables.getFirst(options, null); } - ApiType api = sa.getApi(); - if (null == api) { - throw new InvalidParameterException("SA is not api-based, this is not supported yet"); - } - return SpellApiToAi.Converter.get(api).chooseCounterType(options, sa, params); + return SpellApiToAi.Converter.get(sa).chooseCounterType(options, sa, params); } @Override @@ -1217,7 +1196,7 @@ public class PlayerControllerAi extends PlayerController { @Override public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView allPayers) { - if (SpellApiToAi.Converter.get(sa.getApi()).willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) { + if (SpellApiToAi.Converter.get(sa).willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) { if (!ComputerUtilCost.canPayCost(cost, sa, player, true)) { return false; } @@ -1397,11 +1376,7 @@ public class PlayerControllerAi extends PlayerController { @Override public String chooseCardName(SpellAbility sa, List faces, String message) { - ApiType api = sa.getApi(); - if (null == api) { - throw new InvalidParameterException("SA is not api-based, this is not supported yet"); - } - return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces); + return SpellApiToAi.Converter.get(sa).chooseCardName(player, sa, faces); } @Override @@ -1506,11 +1481,7 @@ public class PlayerControllerAi extends PlayerController { @Override public ICardFace chooseSingleCardFace(SpellAbility sa, List faces, String message) { - ApiType api = sa.getApi(); - if (null == api) { - throw new InvalidParameterException("SA is not api-based, this is not supported yet"); - } - return SpellApiToAi.Converter.get(api).chooseCardFace(player, sa, faces); + return SpellApiToAi.Converter.get(sa).chooseCardFace(player, sa, faces); } @Override @@ -1520,11 +1491,7 @@ public class PlayerControllerAi extends PlayerController { @Override public CardState chooseSingleCardState(SpellAbility sa, List states, String message, Map params) { - ApiType api = sa.getApi(); - if (null == api) { - throw new InvalidParameterException("SA is not api-based, this is not supported yet"); - } - return SpellApiToAi.Converter.get(api).chooseCardState(player, sa, states, params); + return SpellApiToAi.Converter.get(sa).chooseCardState(player, sa, states, params); } @Override @@ -1576,32 +1543,7 @@ public class PlayerControllerAi extends PlayerController { @Override public List chooseOptionalCosts(SpellAbility chosen, List optionalCostValues) { - List chosenOptCosts = Lists.newArrayList(); - Cost costSoFar = chosen.getPayCosts().copy(); - - for (OptionalCostValue opt : optionalCostValues) { - // Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps) - Cost fullCost = opt.getCost().copy().add(costSoFar); - SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost); - - // Playability check for Kicker - if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) { - SpellAbility kickedSaCopy = fullCostSa.copy(); - kickedSaCopy.addOptionalCost(opt.getType()); - Card copy = CardCopyService.getLKICopy(chosen.getHostCard()); - copy.setCastSA(kickedSaCopy); - if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) { - continue; // don't choose kickers we don't want to play - } - } - - if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) { - chosenOptCosts.add(opt); - costSoFar.add(opt.getCost()); - } - } - - return chosenOptCosts; + return SpellApiToAi.Converter.get(chosen).chooseOptionalCosts(chosen, player, optionalCostValues); } @Override @@ -1661,5 +1603,4 @@ public class PlayerControllerAi extends PlayerController { return choices; } - } diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index 89d77c08663..7907eb47e4b 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -13,6 +13,7 @@ import forge.card.mana.ManaCost; import forge.card.mana.ManaCostParser; import forge.game.GameEntity; import forge.game.card.Card; +import forge.game.card.CardCopyService; import forge.game.card.CardState; import forge.game.card.CounterType; import forge.game.cost.Cost; @@ -23,6 +24,8 @@ import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerController.BinaryChoiceType; import forge.game.spellability.AbilitySub; +import forge.game.spellability.OptionalCost; +import forge.game.spellability.OptionalCostValue; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityCondition; import forge.game.zone.ZoneType; @@ -305,7 +308,7 @@ public abstract class SpellAbilityAi { */ public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) { final AbilitySub subAb = ab.getSubAbility(); - return SpellApiToAi.Converter.get(ab.getApi()).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb)); + return SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb)); } public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { @@ -410,4 +413,33 @@ public abstract class SpellAbilityAi { public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map params) { return MyRandom.getRandom().nextBoolean(); } + + public List chooseOptionalCosts(SpellAbility chosen, Player player, List optionalCostValues) { + List chosenOptCosts = Lists.newArrayList(); + Cost costSoFar = chosen.getPayCosts().copy(); + + for (OptionalCostValue opt : optionalCostValues) { + // Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps) + Cost fullCost = opt.getCost().copy().add(costSoFar); + SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost); + + // Playability check for Kicker + if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) { + SpellAbility kickedSaCopy = fullCostSa.copy(); + kickedSaCopy.addOptionalCost(opt.getType()); + Card copy = CardCopyService.getLKICopy(chosen.getHostCard()); + copy.setCastSA(kickedSaCopy); + if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) { + continue; // don't choose kickers we don't want to play + } + } + + if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) { + chosenOptCosts.add(opt); + costSoFar.add(opt.getCost()); + } + } + + return chosenOptCosts; + } } diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java index 93156b84e94..0ad95a13b2c 100644 --- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java @@ -4,8 +4,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import forge.ai.ability.*; import forge.game.ability.ApiType; +import forge.game.spellability.SpellAbility; import forge.util.ReflectionUtil; +import java.security.InvalidParameterException; import java.util.Map; public enum SpellApiToAi { @@ -207,6 +209,14 @@ public enum SpellApiToAi { .put(ApiType.InternalRadiation, AlwaysPlayAi.class) .build()); + public SpellAbilityAi get(final SpellAbility sa) { + ApiType api = sa.getApi(); + if (null == api) { + throw new InvalidParameterException("SA is not api-based, this is not supported yet"); + } + return get(api); + } + public SpellAbilityAi get(final ApiType api) { SpellAbilityAi result = apiToInstance.get(api); if (null == result) { 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 b3e6470c12a..e70273b8013 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -455,7 +455,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } final AbilitySub subAb = sa.getSubAbility(); - return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb); + return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb); } /** @@ -773,7 +773,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } final AbilitySub subAb = sa.getSubAbility(); - return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb); + return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb); } /* @@ -821,7 +821,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } //don't unearth after attacking is possible - if (sa.hasParam("Unearth") && ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) { + if (sa.isKeyword(Keyword.UNEARTH) && ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) { return false; } @@ -941,6 +941,10 @@ public class ChangeZoneAi extends SpellAbilityAi { immediately = immediately || ComputerUtil.playImmediately(ai, sa); + if (list.isEmpty() && immediately && sa.getMaxTargets() == 0) { + return true; + } + // Narrow down the list: if (origin.contains(ZoneType.Battlefield)) { if ("Polymorph".equals(sa.getParam("AILogic"))) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java index dd578c3d128..227bce7282c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java @@ -29,7 +29,7 @@ public class ChooseGenericAi extends SpellAbilityAi { return true; } else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) { for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) { - if (SpellApiToAi.Converter.get(sb.getApi()).canPlayAIWithSubs(ai, sb)) { + if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb)) { return true; } } @@ -93,7 +93,7 @@ public class ChooseGenericAi extends SpellAbilityAi { String unlessCost = sp.getParam("UnlessCost"); sp.setActivatingPlayer(sa.getActivatingPlayer()); Cost unless = new Cost(unlessCost, false); - if (SpellApiToAi.Converter.get(sp.getApi()).willPayUnlessCost(sp, player, unless, false, new FCollection<>(player)) + if (SpellApiToAi.Converter.get(sp).willPayUnlessCost(sp, player, unless, false, new FCollection<>(player)) && ComputerUtilCost.canPayCost(unless, sp, player, true)) { return sp; } @@ -262,7 +262,7 @@ public class ChooseGenericAi extends SpellAbilityAi { List filtered = Lists.newArrayList(); // filter first for the spells which can be done for (SpellAbility sp : spells) { - if (SpellApiToAi.Converter.get(sp.getApi()).canPlayAIWithSubs(player, sp)) { + if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp)) { filtered.add(sp); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ClassLevelUpAi.java b/forge-ai/src/main/java/forge/ai/ability/ClassLevelUpAi.java index ef7594bd094..2e4b32bf240 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ClassLevelUpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ClassLevelUpAi.java @@ -25,7 +25,7 @@ public class ClassLevelUpAi extends SpellAbilityAi { continue; } SpellAbility effect = t.ensureAbility(); - if (!SpellApiToAi.Converter.get(effect.getApi()).doTriggerAI(aiPlayer, effect, false)) { + if (!SpellApiToAi.Converter.get(effect).doTriggerAI(aiPlayer, effect, false)) { return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java index 1a8f4f8385c..2ecf1845723 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java @@ -255,6 +255,9 @@ public class CounterAi extends SpellAbilityAi { } sa.resetTargets(); + if (mandatory && !sa.canAddMoreTarget()) { + return true; + } Pair pair = chooseTargetSpellAbility(game, sa, ai, mandatory); SpellAbility tgtSA = pair.getLeft(); @@ -378,7 +381,7 @@ public class CounterAi extends SpellAbilityAi { } // no reason to pay if we don't plan to confirm - if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered.getApi()).doTriggerNoCostWithSubs(payer, toBeCountered, false)) { + if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false)) { return false; } // TODO check hasFizzled diff --git a/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java index 3c6b9a7eb90..59234bfa3e2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java @@ -27,7 +27,7 @@ public class DelayedTriggerAi extends SpellAbilityAi { trigsa.setActivatingPlayer(ai); if (trigsa instanceof AbilitySub) { - return SpellApiToAi.Converter.get(trigsa.getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa); + return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa); } else { return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); } 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 13192cdeeb5..1d114441b57 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -287,7 +287,7 @@ public class EffectAi extends SpellAbilityAi { } else if (logic.equals("Burn")) { // for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack) SpellAbility burn = sa.getSubAbility(); - return SpellApiToAi.Converter.get(burn.getApi()).canPlayAIWithSubs(ai, burn); + return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn); } else if (logic.equals("YawgmothsWill")) { return SpecialCardAi.YawgmothsWill.consider(ai, sa); } else if (logic.startsWith("NeedCreatures")) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java index aabdf1bf699..eb28a5ad961 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java @@ -24,7 +24,7 @@ public class ImmediateTriggerAi extends SpellAbilityAi { trigsa.setActivatingPlayer(ai); if (trigsa instanceof AbilitySub) { - return SpellApiToAi.Converter.get(trigsa.getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa); + return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa); } else { return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); } diff --git a/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java b/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java index a72bfab20c7..eeb25f8f4f9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java @@ -93,7 +93,7 @@ public class PeekAndRevealAi extends SpellAbilityAi { } AbilitySub subAb = sa.getSubAbility(); - return subAb != null && SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(player, subAb); + return subAb != null && SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(player, subAb); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java index 5916434e971..a3c39d32119 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java @@ -76,7 +76,7 @@ public class TokenAi extends SpellAbilityAi { final AbilitySub sub = sa.getSubAbility(); // useful // no token created - return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub.getApi()).chkAIDrawback(sub, ai)); // planeswalker plus ability or sub-ability is + return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub).chkAIDrawback(sub, ai)); // planeswalker plus ability or sub-ability is } String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString()); diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 39ccab82d0b..aa934c4f481 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -1366,7 +1366,7 @@ public class AbilityUtils { } } - if (source.hasKeyword(Keyword.GIFT) && sa.isOptionalCostPaid(OptionalCost.PromiseGift)) { + if (source.hasKeyword(Keyword.GIFT) && sa.isGiftPromised()) { game.getAction().checkStaticAbilities(); // Is AdditionalAbility available from anything here? AbilitySub giftAbility = (AbilitySub) sa.getAdditionalAbility("GiftAbility"); @@ -2059,6 +2059,9 @@ public class AbilityUtils { if (sq[0].startsWith("Kicked")) { // fallback for not spellAbility return doXMath(calculateAmount(c, sq[!isUnlinkedFromCastSA(ctb, c) && c.getKickerMagnitude() > 0 ? 1 : 2], ctb), expr, c, ctb); } + if (sq[0].startsWith("PromisedGift")) { + return doXMath(calculateAmount(c, sq[c.getCastSA() != null && c.getCastSA().isGiftPromised() ? 1 : 2], ctb), expr, c, ctb); + } if (sq[0].startsWith("Escaped")) { return doXMath(calculateAmount(c, sq[c.getCastSA() != null && c.getCastSA().isEscape() ? 1 : 2], ctb), expr, c, ctb); } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java index 024577d3ee1..66064113a5f 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneEffect.java @@ -11,6 +11,7 @@ import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.*; import forge.game.event.GameEventCombatChanged; +import forge.game.keyword.Keyword; import forge.game.player.*; import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementType; @@ -675,7 +676,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect { if (movedCard.getZone().equals(originZone)) { continue; } - if (sa.hasParam("Unearth") && movedCard.isInPlay()) { + if (sa.isKeyword(Keyword.UNEARTH) && movedCard.isInPlay()) { movedCard.setUnearthed(true); movedCard.addChangedCardKeywords(Lists.newArrayList("Haste"), null, false, game.getNextTimestamp(), null, true); diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 043b0183e9b..39c5131fc90 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -3704,9 +3704,8 @@ public class CardFactoryUtil { String effect = "AB$ ChangeZone | Cost$ " + manacost + " | Defined$ Self" + " | Origin$ Graveyard | Destination$ Battlefield | SorcerySpeed$ True" + - " | ActivationZone$ Graveyard | Unearth$ True | " + - " | PrecostDesc$ Unearth | CostDesc$ " + ManaCostParser.parse(manacost) + " | StackDescription$ " + - "Unearth: Return CARDNAME to the battlefield. | SpellDescription$" + + " | ActivationZone$ Graveyard | PrecostDesc$ Unearth | CostDesc$ " + ManaCostParser.parse(manacost) + + " | StackDescription$ Unearth: Return CARDNAME to the battlefield. | SpellDescription$" + " (" + inst.getReminderText() + ")"; final SpellAbility sa = AbilityFactory.getAbility(effect, card); diff --git a/forge-game/src/main/java/forge/game/card/CardProperty.java b/forge-game/src/main/java/forge/game/card/CardProperty.java index 9f7525a133f..27fd9f84bd8 100644 --- a/forge-game/src/main/java/forge/game/card/CardProperty.java +++ b/forge-game/src/main/java/forge/game/card/CardProperty.java @@ -1826,7 +1826,7 @@ public class CardProperty { if (card.getCastSA() == null) { return false; } - return card.getCastSA().isOptionalCostPaid(OptionalCost.PromiseGift); + return card.getCastSA().isGiftPromised(); } else if (property.equals("impended")) { if (card.getCastSA() == null) { return false; diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 7ea142de0ca..8636182ad54 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -625,6 +625,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit return isOptionalCostPaid(OptionalCost.Jumpstart); } + public boolean isGiftPromised() { + return isOptionalCostPaid(OptionalCost.PromiseGift); + } + public final boolean isBestow() { return isAlternativeCost(AlternativeCost.Bestow); } diff --git a/forge-gui/res/cardsfolder/c/coiling_rebirth.txt b/forge-gui/res/cardsfolder/c/coiling_rebirth.txt index 860556e87a8..8753a29fa1c 100644 --- a/forge-gui/res/cardsfolder/c/coiling_rebirth.txt +++ b/forge-gui/res/cardsfolder/c/coiling_rebirth.txt @@ -4,8 +4,7 @@ Types:Sorcery K:Gift SVar:GiftAbility:DB$ Draw | NumCards$ 1 | Defined$ Promised | GiftDescription$ a card A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | TgtPrompt$ Choose target creature card in your graveyard | ValidTgts$ Creature.YouCtrl | SubAbility$ DBCopy | RememberChanged$ True | SpellDescription$ Return target creature card from your graveyard to the battlefield. Then if the gift was promised and that creature isn't legendary, create a token that's a copy of that creature, except it's 1/1. -SVar:DBCopy:DB$ CopyPermanent | ConditionCheckSVar$ X | ConditionSVarCompare$ EQ2 | Defined$ Remembered | SetPower$ 1 | SetToughness$ 1 | SubAbility$ DBCleanup +SVar:DBCopy:DB$ CopyPermanent | ConditionCheckSVar$ X | ConditionDefined$ Remembered | ConditionPresent$ Card.nonLegendary | Defined$ Remembered | SetPower$ 1 | SetToughness$ 1 | SubAbility$ DBCleanup SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True -SVar:X:Count$ValidStack Card.Self+PromisedGift/Plus.Y -SVar:Y:Count$Valid Card.IsRemembered+nonLegendary +SVar:X:Count$PromisedGift.1.0 Oracle:Gift a card (You may promise an opponent a gift as you cast this spell. If you do, they draw a card before its other effects.)\nReturn target creature card from your graveyard to the battlefield. Then if the gift was promised and that creature isn't legendary, create a token that's a copy of that creature, except it's 1/1. diff --git a/forge-gui/res/cardsfolder/c/consumed_by_greed.txt b/forge-gui/res/cardsfolder/c/consumed_by_greed.txt index 6d42ea943aa..810a060c2a2 100644 --- a/forge-gui/res/cardsfolder/c/consumed_by_greed.txt +++ b/forge-gui/res/cardsfolder/c/consumed_by_greed.txt @@ -5,6 +5,6 @@ K:Gift SVar:GiftAbility:DB$ Draw | NumCards$ 1 | Defined$ Promised | GiftDescription$ a card A:SP$ Sacrifice | ValidTgts$ Opponent | SacValid$ Creature.greatestPowerControlledByTargeted | SacMessage$ the creature with the greatest power | SubAbility$ DBChangeZone | SpellDescription$ Target opponent sacrifices a creature with the greatest power among creatures they control. If the gift was promised, return target creature card from your graveyard to your hand. SVar:DBChangeZone:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | TgtPrompt$ Choose target creature card in your graveyard | TargetMin$ X | TargetMax$ X | ValidTgts$ Creature.YouOwn -SVar:X:Count$ValidStack Card.Self+PromisedGift +SVar:X:Count$PromisedGift.1.0 DeckHas:Ability$Graveyard Oracle:Gift a card (You may promise an opponent a gift as you cast this spell. If you do, they draw a card before its other effects.)\nTarget opponent sacrifices a creature with the greatest power among creatures they control. If the gift was promised, return target creature card from your graveyard to your hand. diff --git a/forge-gui/res/cardsfolder/d/dewdrop_cure.txt b/forge-gui/res/cardsfolder/d/dewdrop_cure.txt index 4e6be93599e..66073de32ec 100644 --- a/forge-gui/res/cardsfolder/d/dewdrop_cure.txt +++ b/forge-gui/res/cardsfolder/d/dewdrop_cure.txt @@ -4,6 +4,6 @@ Types:Sorcery K:Gift SVar:GiftAbility:DB$ Draw | NumCards$ 1 | Defined$ Promised | GiftDescription$ a card A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | TgtPrompt$ Choose target creature card with mana value 2 or less in your graveyard | ValidTgts$ Creature.YouOwn+cmcLE2 | TargetMin$ 0 | TargetMax$ X | SpellDescription$ Return up to two target creature cards each with mana value 2 or less from your graveyard to the battlefield. If the gift was promised, instead return up to three target creature cards each with mana value 2 or less from your graveyard to the battlefield. -SVar:X:Count$ValidStack Card.Self+PromisedGift/Plus.2 +SVar:X:Count$PromisedGift.3.2 DeckHints:Ability$Graveyard|Discard Oracle:Gift a card (You may promise an opponent a gift as you cast this spell. If you do, they draw a card before its other effects.)\nReturn up to two target creature cards each with mana value 2 or less from your graveyard to the battlefield. If the gift was promised, instead return up to three target creature cards each with mana value 2 or less from your graveyard to the battlefield. diff --git a/forge-gui/res/cardsfolder/i/into_the_flood_maw.txt b/forge-gui/res/cardsfolder/i/into_the_flood_maw.txt index 33dcb83d673..7057f3da067 100644 --- a/forge-gui/res/cardsfolder/i/into_the_flood_maw.txt +++ b/forge-gui/res/cardsfolder/i/into_the_flood_maw.txt @@ -4,7 +4,7 @@ Types:Instant K:Gift SVar:GiftAbility:DB$ Token | TokenAmount$ 1 | TokenScript$ u_1_1_fish | TokenTapped$ True | TokenOwner$ Promised | LockTokenScript$ True | GiftDescription$ a tapped Fish A:SP$ ChangeZone | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls | Origin$ Battlefield | Destination$ Hand | TargetMin$ X | TargetMax$ X | SubAbility$ DBChangeZone | SpellDescription$ Return target creature an opponent controls to its owner's hand. If the gift was promised, instead return target nonland permanent an opponent controls to its owner's hand. -SVar:DBChangeZone:DB$ ChangeZone | ValidTgts$ Permanent.nonLand+OppCtrl | TgtPrompt$ Select target nonland permanent an opponent controls | Origin$ Battlefield | Destination$ Hand | TargetMin$ Y | TargetMax$ Y -SVar:X:Count$Compare Y EQ1.0.1 -SVar:Y:Count$ValidStack Card.Self+PromisedGift +SVar:DBChangeZone:DB$ ChangeZone | ValidTgts$ Permanent.nonLand+OppCtrl | TgtPrompt$ Select target nonland permanent an opponent controls | Origin$ Battlefield | Destination$ Hand | TargetMin$ Y | TargetMax$ Y | AITgts$ !Creature +SVar:X:Count$PromisedGift.0.1 +SVar:Y:Count$PromisedGift.1.0 Oracle:Gift a tapped Fish (You may promise an opponent a gift as you cast this spell. If you do, they create a tapped 1/1 blue Fish creature token before its other effects.)\nReturn target creature an opponent controls to its owner's hand. If the gift was promised, instead return target nonland permanent an opponent controls to its owner's hand. diff --git a/forge-gui/res/cardsfolder/l/long_rivers_pull.txt b/forge-gui/res/cardsfolder/l/long_rivers_pull.txt index 57689ffe434..745187cf81a 100644 --- a/forge-gui/res/cardsfolder/l/long_rivers_pull.txt +++ b/forge-gui/res/cardsfolder/l/long_rivers_pull.txt @@ -4,7 +4,7 @@ Types:Instant K:Gift SVar:GiftAbility:DB$ Draw | NumCards$ 1 | Defined$ Promised | GiftDescription$ a card A:SP$ Counter | TargetType$ Spell | TgtPrompt$ Select target creature spell | ValidTgts$ Creature | TargetMin$ X | TargetMax$ X | SubAbility$ DBCounter | SpellDescription$ Counter target creature spell. If the gift was promised, instead counter target spell. -SVar:DBCounter:DB$ Counter | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | TargetMin$ Y | TargetMax$ Y -SVar:X:Count$Compare Y EQ1.0.1 -SVar:Y:Count$ValidStack Card.Self+PromisedGift +SVar:DBCounter:DB$ Counter | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | TargetMin$ Y | TargetMax$ Y | AITgts$ !Creature +SVar:X:Count$PromisedGift.0.1 +SVar:Y:Count$PromisedGift.1.0 Oracle:Gift a card (You may promise an opponent a gift as you cast this spell. If you do, they draw a card before its other effects.)\nCounter target creature spell. If the gift was promised, instead counter target spell. diff --git a/forge-gui/res/cardsfolder/m/mind_spiral.txt b/forge-gui/res/cardsfolder/m/mind_spiral.txt index e8feae99281..34e8baffec3 100644 --- a/forge-gui/res/cardsfolder/m/mind_spiral.txt +++ b/forge-gui/res/cardsfolder/m/mind_spiral.txt @@ -6,6 +6,6 @@ SVar:GiftAbility:DB$ Token | TokenAmount$ 1 | TokenScript$ u_1_1_fish | TokenTap A:SP$ Draw | NumCards$ 3 | ValidTgts$ Player | TgtPrompt$ Select target player | SubAbility$ DBTap | SpellDescription$ Target player draws three cards. If the gift was promised, tap target creature an opponent controls and put a stun counter on it. (If a permanent with a stun counter would become untapped, remove one from it instead.) SVar:DBTap:DB$ Tap | ValidTgts$ Creature.OppCtrl | TargetMin$ X | TargetMax$ X | SubAbility$ DBCounter SVar:DBCounter:DB$ PutCounter | Defined$ ParentTarget | ConditionZone$ Stack | ConditionPresent$ Card.Self+PromisedGift | ConditionCompare$ EQ1 | CounterType$ Stun | CounterNum$ 1 -SVar:X:Count$ValidStack Card.Self+PromisedGift +SVar:X:Count$PromisedGift.1.0 DeckHas:Ability$Counters Oracle:Gift a tapped Fish (You may promise an opponent a gift as you cast this spell. If you do, they create a tapped 1/1 blue Fish creature token before its other effects.)\nTarget player draws three cards. If the gift was promised, tap target creature an opponent controls and put a stun counter on it. (If a permanent with a stun counter would become untapped, remove one from it instead.) diff --git a/forge-gui/res/cardsfolder/p/peerless_recycling.txt b/forge-gui/res/cardsfolder/p/peerless_recycling.txt index fd54e0d43ef..5e21cdaf8e2 100644 --- a/forge-gui/res/cardsfolder/p/peerless_recycling.txt +++ b/forge-gui/res/cardsfolder/p/peerless_recycling.txt @@ -4,5 +4,5 @@ Types:Instant K:Gift SVar:GiftAbility:DB$ Draw | NumCards$ 1 | Defined$ Promised | GiftDescription$ a card A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Hand | TargetMin$ X | TargetMax$ X | TgtPrompt$ Choose target permanent card in your graveyard | ValidTgts$ Permanent.YouCtrl | SpellDescription$ Return target permanent card from your graveyard to your hand. If the gift was promised, instead return two target permanent cards from your graveyard to your hand. -SVar:X:Count$ValidStack Card.Self+PromisedGift/Plus.1 +SVar:X:Count$PromisedGift.2.1 Oracle:Gift a card (You may promise an opponent a gift as you cast this spell. If you do, they draw a card before its other effects.)\nReturn target permanent card from your graveyard to your hand. If the gift was promised, instead return two target permanent cards from your graveyard to your hand. diff --git a/forge-gui/res/cardsfolder/s/sazacaps_brew.txt b/forge-gui/res/cardsfolder/s/sazacaps_brew.txt index 18af0ff3912..735f3f58de4 100644 --- a/forge-gui/res/cardsfolder/s/sazacaps_brew.txt +++ b/forge-gui/res/cardsfolder/s/sazacaps_brew.txt @@ -5,7 +5,7 @@ K:Gift SVar:GiftAbility:DB$ Token | TokenAmount$ 1 | TokenScript$ u_1_1_fish | TokenTapped$ True | TokenOwner$ Promised | LockTokenScript$ True | GiftDescription$ a tapped Fish A:SP$ Draw | Cost$ 1 R Discard<1/Card> | CostDesc$ As an additional cost to cast this spell, discard a card. | NumCards$ 2 | ValidTgts$ Player | TgtPrompt$ Select target player | SubAbility$ DBPump | SpellDescription$ Target player draws two cards. If the gift was promised, target creature you control gets +2/+0 until end of turn. SVar:DBPump:DB$ Pump | ValidTgts$ Creature.YouCtrl | TargetMin$ X | TargetMax$ X | TgtPrompt$ Select target creature you control | NumAtt$ +2 -SVar:X:Count$ValidStack Card.Self+PromisedGift +SVar:X:Count$PromisedGift.1.0 DeckHas:Ability$Discard DeckHints:Keyword$Madness & Ability$Delirium Oracle:Gift a tapped Fish (You may promise an opponent a gift as you cast this spell. If you do, they create a tapped 1/1 blue Fish creature token before its other effects.)\nAs an additional cost to cast this spell, discard a card.\nTarget player draws two cards. If the gift was promised, target creature you control gets +2/+0 until end of turn. diff --git a/forge-gui/res/cardsfolder/v/valley_rally.txt b/forge-gui/res/cardsfolder/v/valley_rally.txt index 052d5430133..6c492dda61e 100644 --- a/forge-gui/res/cardsfolder/v/valley_rally.txt +++ b/forge-gui/res/cardsfolder/v/valley_rally.txt @@ -5,5 +5,5 @@ K:Gift SVar:GiftAbility:DB$ Token | TokenAmount$ 1 | TokenScript$ c_a_food_sac | TokenOwner$ Promised | GiftDescription$ a Food A:SP$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +2 | SubAbility$ DBPump | SpellDescription$ Creatures you control get +2/+0 until end of turn. If the gift was promised, target creature you control gains first strike until end of turn. SVar:DBPump:DB$ Pump | ValidTgts$ Creature.YouCtrl | TargetMin$ X | TargetMax$ X | TgtPrompt$ Select target creature you control | KW$ First Strike -SVar:X:Count$ValidStack Card.Self+PromisedGift +SVar:X:Count$PromisedGift.1.0 Oracle:Gift a Food (You may promise an opponent a gift as you cast this spell. If you do, they create a Food token before its other effects. It's an artifact with "{2}, {T}, Sacrifice this artifact: You gain 3 life.")\nCreatures you control get +2/+0 until end of turn. If the gift was promised, target creature you control gains first strike until end of turn. diff --git a/forge-gui/res/cardsfolder/w/wear_down.txt b/forge-gui/res/cardsfolder/w/wear_down.txt index 37b3641b3ef..044ca71fd22 100644 --- a/forge-gui/res/cardsfolder/w/wear_down.txt +++ b/forge-gui/res/cardsfolder/w/wear_down.txt @@ -4,5 +4,5 @@ Types:Sorcery K:Gift SVar:GiftAbility:DB$ Draw | NumCards$ 1 | Defined$ Promised | GiftDescription$ a card A:SP$ Destroy | ValidTgts$ Artifact,Enchantment | TargetMin$ X | TargetMax$ X | TgtPrompt$ Select target artifact or enchantment | SpellDescription$ Destroy target artifact or enchantment. If the gift was promised, instead destroy two target artifacts and/or enchantments. -SVar:X:Count$ValidStack Card.Self+PromisedGift/Plus.1 +SVar:X:Count$PromisedGift.2.1 Oracle:Gift a card (You may promise an opponent a gift as you cast this spell. If you do, they draw a card before its other effects.)\nDestroy target artifact or enchantment. If the gift was promised, instead destroy two target artifacts and/or enchantments. diff --git a/forge-gui/res/cardsfolder/w/wildfire_howl.txt b/forge-gui/res/cardsfolder/w/wildfire_howl.txt index e4340501a22..9e1609fa2ef 100644 --- a/forge-gui/res/cardsfolder/w/wildfire_howl.txt +++ b/forge-gui/res/cardsfolder/w/wildfire_howl.txt @@ -6,5 +6,5 @@ SVar:GiftAbility:DB$ Draw | NumCards$ 1 | Defined$ Promised | GiftDescription$ a A:SP$ DealDamage | ValidTgts$ Any | NumDmg$ 1 | TargetMin$ X | TargetMax$ X | SubAbility$ DBDamageAll | DamageMap$ True | SpellDescription$ CARDNAME deals 2 damage to each creature. If the gift was promised, instead CARDNAME deals 1 damage to any target and 2 damage to each creature. SVar:DBDamageAll:DB$ DamageAll | NumDmg$ 2 | ValidCards$ Creature | ValidDescription$ each creature | SubAbility$ DBDamageResolve SVar:DBDamageResolve:DB$ DamageResolve -SVar:X:Count$ValidStack Card.Self+PromisedGift +SVar:X:Count$PromisedGift.1.0 Oracle:Gift a card (You may promise an opponent a gift as you cast this spell. If you do, they draw a card before its other effects.)\nWildfire Howl deals 2 damage to each creature. If the gift was promised, instead Wildfire Howl deals 1 damage to any target and 2 damage to each creature.