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 @@ private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) { 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 AiPlayDecision canPlaySa(SpellAbility sa) { 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 AiPlayDecision canPlayFromEffectAI(Spell spell, boolean mandatory, boolea 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 @@ private SpellAbility chooseSpellAbilityToPlayFromList(final List a 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; + 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; + + // 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) { + } 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 boolean doTrigger(SpellAbility spell, boolean mandatory) { 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 static CardCollection choosePermanentsToSacrifice(final Player ai, final // 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.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 static List getOriginalAndAltCostAbilities(final 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 static CardCollection getAvailableManaSources(final Player ai, final bool 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 @@ private static ListMultimap groupSourcesByManaColor(final // 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.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 T chooseSingleEntityForEffect(FCollectionView o 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 List chooseSpellAbilitiesForEffect(List spell @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 int chooseNumber(SpellAbility sa, String title, int min, int max) { @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 boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType k */ @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 CounterType chooseCounterType(List options, SpellAbility sa, 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 boolean payCombatCost(Card c, Cost cost, SpellAbility sa, String prompt) @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 Map chooseCardsForConvokeOrImprovise(SpellAbility sa @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 void cancelAwaitNextInput() { @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 ICardFace chooseSingleCardFace(SpellAbility sa, String message, Predicate @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 List chooseCardsForSplice(SpellAbility sa, List cards) { @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 CardCollection chooseCardsForEffectMultiple(Map v 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.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.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 @@ protected static boolean playReusable(final Player ai, final SpellAbility sa) { */ 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 CounterType chooseCounterType(List options, SpellAbility sa, 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.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 @@ private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility } 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 @@ private static boolean knownOriginCanPlayAI(final Player ai, final SpellAbility } 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 @@ protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandle } //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 @@ private static boolean isPreferredTarget(final Player ai, final SpellAbility sa, 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 @@ protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final Str 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 SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, Lis 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 SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, Lis 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 @@ protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { 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 @@ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandator } sa.resetTargets(); + if (mandatory && !sa.canAddMoreTarget()) { + return true; + } Pair pair = chooseTargetSpellAbility(game, sa, ai, mandatory); SpellAbility tgtSA = pair.getLeft(); @@ -378,7 +381,7 @@ public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boole } // 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 boolean chkAIDrawback(SpellAbility sa, Player ai) { 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 @@ protected boolean canPlayAI(final Player ai,final SpellAbility sa) { } 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 boolean chkAIDrawback(SpellAbility sa, Player ai) { 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 boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirm } 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 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, 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 @@ private static void resolvePreAbilities(final SpellAbility sa, final Game game) } } - 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 static int xCount(Card c, final String s, final CardTraitBase ctb) { 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.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 @@ private void changeKnownOriginResolve(final SpellAbility sa) { 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 void resolve() { 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 @@ else if (property.equals("blocked")) { 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 boolean isJumpstart() { 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.