Extra cost gift rework (#6733)

* added PromisedGift as xCount

* SpellAbilityAi: move chooseOptionalCosts

* SpellAbilityAi: chooseOptionalCosts check for invalid targets

* Give API logic access to castSA

* ~ add BaseSpell for PromiseGift

* Unearth: remove unneeded Param

* PromiseGift: uses AITgts to stop AI from using Gift when not needed

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
This commit is contained in:
Hans Mackowiak
2025-01-05 08:52:35 +01:00
committed by GitHub
parent 1209f39cf1
commit 2fcf95c755
32 changed files with 156 additions and 142 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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<OptionalCostValue> 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;

View File

@@ -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;
}
}

View File

@@ -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<T>)optionList, isOptional, targetedPlayer, params);
return SpellApiToAi.Converter.get(sa).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
}
@Override
@@ -398,11 +393,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> 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<String, Object> 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<String, Object> 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<Player> 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<ICardFace> 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<ICardFace> 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<CardState> states, String message, Map<String, Object> 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<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> 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;
}
}

View File

@@ -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<String, Object> params) {
@@ -410,4 +413,33 @@ public abstract class SpellAbilityAi {
public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map<String, Object> params) {
return MyRandom.getRandom().nextBoolean();
}
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, Player player, List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> 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;
}
}

View File

@@ -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) {

View File

@@ -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"))) {

View File

@@ -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<SpellAbility> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -255,6 +255,9 @@ public class CounterAi extends SpellAbilityAi {
}
sa.resetTargets();
if (mandatory && !sa.canAddMoreTarget()) {
return true;
}
Pair<SpellAbility, Boolean> 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

View File

@@ -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);
}

View File

@@ -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")) {

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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());

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.)

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.