Merge branch '1659-npe-at-forge-game-ability-abilityutils-getdefinedplayers-abilityutils-java-1084' into 'master'

CostRemoveCounter + CostRemoveAnyCounter: refactor remove X counters from something you control

Closes #1385 and #1660

See merge request core-developers/forge!3506
This commit is contained in:
Michael Kamensky
2020-12-23 08:05:26 +00:00
86 changed files with 835 additions and 983 deletions

View File

@@ -751,7 +751,7 @@ public class AiController {
if (xPay <= 0) {
return AiPlayDecision.CantAffordX;
}
card.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
} else if (mana.isZero()) {
// if mana is zero, but card mana cost does have X, then something is wrong
ManaCost cardCost = card.getManaCost();
@@ -1061,8 +1061,8 @@ public class AiController {
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
} else if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
final int CMC = Integer.parseInt(sourceCard.getSVar("PayX"));
CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(CMC));
final int cmc = Integer.parseInt(sa.getSVar("PayX"));
CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc));
if (discards.isEmpty()) {
return null;
} else {

View File

@@ -4,12 +4,15 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ObjectUtils;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import forge.card.CardType;
import forge.game.Game;
import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -20,6 +23,7 @@ import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
@@ -632,60 +636,162 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.card(aic.getCardsToDiscard(c, type.split(";"), ability));
}
protected int removeCounter(GameEntityCounterTable table, List<Card> prefs, CounterEnumType cType, int stillToRemove) {
int removed = 0;
if (!prefs.isEmpty() && stillToRemove > 0) {
Collections.sort(prefs, CardPredicates.compareByCounterType(cType));
for (Card prefCard : prefs) {
// already enough removed
if (stillToRemove <= removed) {
break;
}
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) {
removed += thisRemove;
table.put(prefCard, CounterType.get(cType), thisRemove);
}
}
}
return removed;
}
@Override
public PaymentDecision visit(CostRemoveAnyCounter cost) {
final String amount = cost.getAmount();
final int c = AbilityUtils.calculateAmount(source, amount, ability);
final String type = cost.getType();
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
CardCollectionView typeList = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"), player, source, ability);
if (c <= 0) {
return null;
}
CardCollectionView typeList = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), cost.getType().split(";"), player, source, ability);
// only cards with counters are of interest
typeList = CardLists.filter(typeList, CardPredicates.hasCounters());
// no target
if (typeList.isEmpty()) {
return null;
}
// TODO fill up a GameEntityCounterTable
// cost now has counter type or null
// the amount might be different from 1, could be X
// currently if amount is bigger than one,
// it tries to remove all counters from one source and type at once
int toRemove = 0;
final GameEntityCounterTable table = new GameEntityCounterTable();
// currently the only one using remove any counter using a type uses p1p1
// the first things are benefit from removing counters
// try to remove +1/+1 counter from undying creature
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.P1P1, c),
CardPredicates.hasKeyword("Undying"));
if (!prefs.isEmpty()) {
Collections.sort(prefs, CardPredicates.compareByCounterType(CounterEnumType.P1P1));
PaymentDecision result = PaymentDecision.card(prefs);
result.ct = CounterType.get(CounterEnumType.P1P1);
return result;
}
// try to remove -1/-1 counter from persist creature
prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.M1M1, c),
CardPredicates.hasKeyword("Persist"));
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.M1M1))) {
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.M1M1), CardPredicates.hasKeyword(Keyword.PERSIST));
if (!prefs.isEmpty()) {
Collections.sort(prefs, CardPredicates.compareByCounterType(CounterEnumType.M1M1));
PaymentDecision result = PaymentDecision.card(prefs);
result.ct = CounterType.get(CounterEnumType.M1M1);
return result;
toRemove += removeCounter(table, prefs, CounterEnumType.M1M1, c - toRemove);
}
// try to remove Time counter from Chronozoa, it will generate more
prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.TIME, c),
CardPredicates.nameEquals("Chronozoa"));
// try to remove +1/+1 counter from undying creature
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.P1P1))) {
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.P1P1), CardPredicates.hasKeyword(Keyword.UNDYING));
if (!prefs.isEmpty()) {
Collections.sort(prefs, CardPredicates.compareByCounterType(CounterEnumType.TIME));
PaymentDecision result = PaymentDecision.card(prefs);
result.ct = CounterType.get(CounterEnumType.TIME);
return result;
toRemove += removeCounter(table, prefs, CounterEnumType.P1P1, c - toRemove);
}
if (c > toRemove && cost.counter == null && originalHost.hasSVar("AIRemoveCounterCostPriority") && !"ANY".equalsIgnoreCase(originalHost.getSVar("AIRemoveCounterCostPriority"))) {
String[] counters = TextUtil.split(originalHost.getSVar("AIRemoveCounterCostPriority"), ',');
for (final String ctr : counters) {
CounterType ctype = CounterType.getType(ctr);
// ctype == null means any type
// any type is just used to return null for this
for (Card card : CardLists.filter(typeList, CardPredicates.hasCounter(ctype))) {
int thisRemove = Math.min(card.getCounters(ctype), c - toRemove);
if (thisRemove > 0) {
toRemove += thisRemove;
table.put(card, ctype, thisRemove);
}
}
}
}
// filter for negative counters
if (c > toRemove && cost.counter == null) {
List<Card> negatives = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (CounterType cType : table.filterToRemove(crd).keySet()) {
if (ComputerUtil.isNegativeCounter(cType, crd)) {
return true;
}
}
return false;
}
});
if (!negatives.isEmpty()) {
// TODO sort negatives to remove from best Cards first?
for (final Card crd : negatives) {
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(crd).entrySet()) {
if (ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
int over = Math.min(e.getValue(), c - toRemove);
if (over > 0) {
toRemove += over;
table.put(crd, e.getKey(), over);
}
}
}
}
}
}
// filter for useless counters
// they have no effect on the card, if they are there or removed
if (c > toRemove && cost.counter == null) {
List<Card> useless = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (CounterType ctype : table.filterToRemove(crd).keySet()) {
if (ComputerUtil.isUselessCounter(ctype, crd)) {
return true;
}
}
return false;
}
});
if (!useless.isEmpty()) {
for (final Card crd : useless) {
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(crd).entrySet()) {
if (ComputerUtil.isUselessCounter(e.getKey(), crd)) {
int over = Math.min(e.getValue(), c - toRemove);
if (over > 0) {
toRemove += over;
table.put(crd, e.getKey(), over);
}
}
}
}
}
}
// try to remove Time counter from Chronozoa, it will generate more token
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.TIME))) {
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.TIME), CardPredicates.nameEquals("Chronozoa"));
toRemove += removeCounter(table, prefs, CounterEnumType.TIME, c - toRemove);
}
// try to remove Quest counter on something with enough counters for the
// effect to continue
prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.QUEST, c));
if (!prefs.isEmpty()) {
prefs = CardLists.filter(prefs, new Predicate<Card>() {
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.QUEST))) {
List<Card> prefs = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
// a Card without MaxQuestEffect doesn't need any Quest
@@ -694,130 +800,65 @@ public class AiCostDecision extends CostDecisionMakerBase {
if (crd.hasSVar("MaxQuestEffect")) {
e = Integer.parseInt(crd.getSVar("MaxQuestEffect"));
}
return crd.getCounters(CounterEnumType.QUEST) >= e + c;
return crd.getCounters(CounterEnumType.QUEST) > e;
}
});
Collections.sort(prefs, Collections.reverseOrder(CardPredicates.compareByCounterType(CounterEnumType.QUEST)));
PaymentDecision result = PaymentDecision.card(prefs);
result.ct = CounterType.get(CounterEnumType.QUEST);
return result;
for (final Card crd : prefs) {
int e = 0;
if (crd.hasSVar("MaxQuestEffect")) {
e = Integer.parseInt(crd.getSVar("MaxQuestEffect"));
}
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) {
toRemove += over;
table.put(crd, CounterType.get(CounterEnumType.QUEST), over);
}
}
}
// filter for only cards with enough counters
typeList = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (Integer i : crd.getCounters().values()) {
if (i >= c) {
return true;
// remove Lore counters from Sagas to keep them longer
if (c > toRemove && (cost.counter == null || cost.counter.is(CounterEnumType.LORE))) {
List<Card> prefs = CardLists.filter(typeList, CardPredicates.hasCounter(CounterEnumType.LORE), CardPredicates.isType("Saga"));
// TODO add Svars and other stuff to keep the Sagas on specific levels
// also add a way for the AI to respond to the last Chapter ability to keep the Saga on the field if wanted
toRemove += removeCounter(table, prefs, CounterEnumType.LORE, c - toRemove);
}
// TODO add logic to remove positive counters?
if (c > toRemove && cost.counter != null) {
// TODO add logic for Ooze Flux, should probably try to make a token as big as possible
// without killing own non undying creatures in the process
// the amount of X should probably be tweaked for this
List<Card> withCtr = CardLists.filter(typeList, CardPredicates.hasCounter(cost.counter));
for (Card card : withCtr) {
int thisRemove = Math.min(card.getCounters(cost.counter), c - toRemove);
if (thisRemove > 0) {
toRemove += thisRemove;
table.put(card, cost.counter, thisRemove);
}
}
}
// Used to not return null
// Special part for CostPriority Any
if (c > toRemove && cost.counter == null && originalHost.hasSVar("AIRemoveCounterCostPriority") && "ANY".equalsIgnoreCase(originalHost.getSVar("AIRemoveCounterCostPriority"))) {
for (Card card : typeList) {
// TODO try not to remove to much positive counters from the same card
for (Map.Entry<CounterType, Integer> e : table.filterToRemove(card).entrySet()) {
int thisRemove = Math.min(e.getValue(), c - toRemove);
if (thisRemove > 0) {
toRemove += thisRemove;
table.put(card, e.getKey(), thisRemove);
}
}
return false;
}
});
// nothing with enough counters of any type
if (typeList.isEmpty()) {
return null;
}
// filter for negative counters
List<Card> negatives = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
if (e.getValue() >= c && ComputerUtil.isNegativeCounter(e.getKey(), crd)) {
return true;
}
}
return false;
}
});
if (!negatives.isEmpty()) {
final Card card = ComputerUtilCard.getBestAI(negatives);
PaymentDecision result = PaymentDecision.card(card);
for (Map.Entry<CounterType, Integer> e : card.getCounters().entrySet()) {
if (e.getValue() >= c && ComputerUtil.isNegativeCounter(e.getKey(), card)) {
result.ct = e.getKey();
break;
}
}
return result;
}
// filter for useless counters
// they have no effect on the card, if they are there or removed
List<Card> useless = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
if (e.getValue() >= c && ComputerUtil.isUselessCounter(e.getKey())) {
return true;
}
}
return false;
}
});
if (!useless.isEmpty()) {
final Card card = useless.get(0);
PaymentDecision result = PaymentDecision.card(card);
for (Map.Entry<CounterType, Integer> e : card.getCounters().entrySet()) {
if (e.getValue() >= c && ComputerUtil.isUselessCounter(e.getKey())) {
result.ct = e.getKey();
break;
}
}
return result;
}
// try a way to pay unless cost
if ("Chisei, Heart of Oceans".equals(ComputerUtilAbility.getAbilitySourceName(ability))) {
final Card card = ComputerUtilCard.getWorstAI(typeList);
PaymentDecision result = PaymentDecision.card(card);
for (Map.Entry<CounterType, Integer> e : card.getCounters().entrySet()) {
if (e.getValue() >= c) {
result.ct = e.getKey();
break;
}
}
return result;
}
// check if the card defines its own priorities for counter removal as cost
if (source.hasSVar("AIRemoveCounterCostPriority")) {
String[] counters = TextUtil.split(source.getSVar("AIRemoveCounterCostPriority"), ',');
for (final String ctr : counters) {
List<Card> withCtr = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card crd) {
for (Map.Entry<CounterType, Integer> e : crd.getCounters().entrySet()) {
if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey().equals(CounterType.getType(ctr)))) {
return true;
}
}
return false;
}
});
if (!withCtr.isEmpty()) {
final Card card = withCtr.get(0);
PaymentDecision result = PaymentDecision.card(card);
for (Map.Entry<CounterType, Integer> e : card.getCounters().entrySet()) {
if (e.getValue() >= c && (ctr.equals("ANY") || e.getKey().equals(CounterType.getType(ctr)))) {
result.ct = e.getKey();
break;
}
}
return result;
}
}
}
return null;
// if table is empty, than no counter was removed
return table.isEmpty() ? null : PaymentDecision.counters(table);
}
@Override
@@ -843,7 +884,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
}
} else if (sVar.equals("Count$xPaid")) {
c = AbilityUtils.calculateAmount(source, "PayX", null);
c = AbilityUtils.calculateAmount(source, "PayX", ability);
} else {
c = AbilityUtils.calculateAmount(source, amount, ability);
}

View File

@@ -161,8 +161,6 @@ public class ComputerUtil {
// Play higher costing spells first?
final Cost cost = sa.getPayCosts();
// Convert cost to CMC
// String totalMana = source.getSVar("PayX"); // + cost.getCMC()
// Consider the costs here for relative "scoring"
if (hasDiscardHandCost(cost)) {
@@ -2809,7 +2807,17 @@ public class ComputerUtil {
}
// this countertypes has no effect
public static boolean isUselessCounter(CounterType type) {
public static boolean isUselessCounter(CounterType type, Card c) {
// Quest counter on a card without MaxQuestEffect are useless
if (type.is(CounterEnumType.QUEST)) {
int e = 0;
if ( c.hasSVar("MaxQuestEffect")) {
e = Integer.parseInt(c.getSVar("MaxQuestEffect"));
}
return c.getCounters(type) > e;
}
return type.is(CounterEnumType.AWAKENING) || type.is(CounterEnumType.MANIFESTATION) || type.is(CounterEnumType.PETRIFICATION)
|| type.is(CounterEnumType.TRAINING);
}

View File

@@ -19,6 +19,8 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
@@ -53,9 +55,6 @@ public class ComputerUtilCost {
return true;
}
public static boolean checkRemoveCounterCost(final Cost cost, final Card source) {
return checkRemoveCounterCost(cost, source, null);
}
/**
* Check remove counter cost.
*
@@ -69,6 +68,7 @@ public class ComputerUtilCost {
if (cost == null) {
return true;
}
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa);
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
@@ -88,17 +88,17 @@ public class ComputerUtilCost {
// Remove X counters - set ChosenX to max possible value here, the SAs should correct that
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
if (sa.hasSVar(remCounter.getAmount())) {
final String sVar = sa.getSVar(remCounter.getAmount());
if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) {
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
} else if (sVar.equals("Count$xPaid") && sa.hasSVar("PayX")) {
sa.setSVar("PayX", Integer.toString(Math.min(Integer.valueOf(sa.getSVar("PayX")), source.getCounters(type))));
}
}
// check the sa what the PaymentDecision is.
// ignore Loyality abilities with Zero as Cost
if (sa != null && !type.is(CounterEnumType.LOYALTY)) {
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa);
if (!type.is(CounterEnumType.LOYALTY)) {
PaymentDecision pay = decision.visit(remCounter);
if (pay == null || pay.c <= 0) {
return false;
@@ -111,14 +111,10 @@ public class ComputerUtilCost {
return false;
}
} else if (part instanceof CostRemoveAnyCounter) {
if (sa != null) {
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
PaymentDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa).visit(remCounter);
return decision != null;
}
return false;
PaymentDecision pay = decision.visit(remCounter);
return pay != null;
}
}
return true;
@@ -677,4 +673,26 @@ public class ComputerUtilCost {
}
return false;
}
public static int getMaxXValue(SpellAbility sa, Player ai) {
final Cost abCost = sa.getPayCosts();
if (abCost == null || !abCost.hasXInAnyCostPart()) {
return 0;
}
Integer val = null;
if (sa.costHasManaX()) {
val = ComputerUtilMana.determineLeftoverMana(sa, ai);
}
if (sa.usesTargeting()) {
// if announce is used as min targets, check what the max possible number would be
if ("X".equals(sa.getTargetRestrictions().getMinTargets())) {
val = ObjectUtils.min(val, CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa).size());
}
}
return ObjectUtils.defaultIfNull(ObjectUtils.min(val, abCost.getMaxForNonManaX(sa, ai)), 0);
}
}

View File

@@ -4,7 +4,6 @@ import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
import forge.ai.ability.AnimateAi;
import forge.card.CardStateName;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
@@ -119,10 +118,6 @@ public class ComputerUtilMana {
return score;
}
private static void sortManaAbilities(final Multimap<ManaCostShard, SpellAbility> manaAbilityMap) {
sortManaAbilities(manaAbilityMap, null);
}
private static void sortManaAbilities(final Multimap<ManaCostShard, SpellAbility> manaAbilityMap, final SpellAbility sa) {
final Map<Card, Integer> manaCardMap = Maps.newHashMap();
final List<Card> orderedCards = Lists.newArrayList();
@@ -1194,11 +1189,11 @@ public class ComputerUtilMana {
} else {
// For Count$xPaid set PayX in the AFs then use that here
// Else calculate it as appropriate.
final String xSvar = card.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X";
if (!sa.getSVar(xSvar).isEmpty() || card.hasSVar(xSvar) || card.getState(CardStateName.Original).hasSVar(xSvar)) {
if (xSvar.equals("PayX") && (card.hasSVar(xSvar) || card.getState(CardStateName.Original).hasSVar(xSvar))) {
final String xSvar = sa.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X";
if (sa.hasSVar(xSvar)) {
if (xSvar.equals("PayX")) {
// X SVar may end up being an empty string when copying a spell with no cost (e.g. Jhoira Avatar)
String xValue = card.hasSVar(xSvar) ? card.getSVar(xSvar) : card.getState(CardStateName.Original).getSVar(xSvar);
String xValue = sa.getSVar(xSvar);
manaToAdd = xValue.isEmpty() ? 0 : Integer.parseInt(xValue) * cost.getXcounter(); // X
} else {
manaToAdd = AbilityUtils.calculateAmount(card, xSvar, sa) * cost.getXcounter();
@@ -1206,9 +1201,7 @@ public class ComputerUtilMana {
}
}
String manaXColor = sa.getParam("XColor");
ManaCostShard shardToGrow = ManaCostShard.parseNonGeneric(manaXColor == null ? "1" : manaXColor);
cost.increaseShard(shardToGrow, manaToAdd);
cost.increaseShard(ManaCostShard.parseNonGeneric(sa.getParamOrDefault("XColor", "1")), manaToAdd);
if (!test) {
sa.setXManaCostPaid(manaToAdd / cost.getXcounter());

View File

@@ -875,7 +875,7 @@ public class SpecialCardAi {
tokenSize = 11;
}
source.setSVar("PayX", Integer.toString(tokenSize));
sa.setSVar("PayX", Integer.toString(tokenSize));
return true;
}

View File

@@ -78,12 +78,6 @@ public abstract class SpellAbilityAi {
}
}
if (sa.hasParam("AITgtBeforeCostEval")) {
// Cost payment requires a valid target to be specified, e.g. Quillmane Baku, so run the API logic first
// to set the target, then decide on paying costs (slower, so only use for cards where it matters)
return checkApiLogic(ai, sa) && (cost == null || willPayCosts(ai, sa, cost, source));
}
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
return false;
}

View File

@@ -93,7 +93,7 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
if (abCost.getTotalMana().countX() > 0 && source.getSVar("X").equals("Count$xPaid")) {
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. (Endless Scream and Venarian
// Gold)
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
@@ -102,7 +102,7 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
}
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Chained to the Rocks")) {

View File

@@ -339,10 +339,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
String type = sa.getParam("ChangeType");
if (type != null) {
if (type.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
type = type.replace("X", Integer.toString(xPay));
}
}
@@ -384,11 +384,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
String num = sa.getParam("ChangeNum");
if (num != null) {
if (num.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (num.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
xPay = Math.min(xPay, list.size());
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
}
}
@@ -474,8 +474,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
final Card source = sa.getHostCard();
if (sa.hasParam("AILogic")) {
if (sa.getParam("AILogic").equals("Never")) {
/*
@@ -496,10 +494,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
// this works for hidden because the mana is paid first.
final String type = sa.getParam("ChangeType");
if (type != null && type.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (type != null && type.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
}
Iterable<Player> pDefined;
@@ -1830,7 +1828,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
int toPay = 0;
boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else {
@@ -1846,7 +1844,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (setPayX) {
source.setSVar("PayX", Integer.toString(toPay));
sa.setSVar("PayX", Integer.toString(toPay));
}
}
}

View File

@@ -44,7 +44,7 @@ public class ChooseColorAi extends SpellAbilityAi {
}
// Set PayX here to maximum value.
int x = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(x));
sa.setSVar("PayX", Integer.toString(x));
return true;
}

View File

@@ -60,7 +60,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) {
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) {
return false;
}
}

View File

@@ -280,7 +280,7 @@ public class ControlGainAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, final Player ai) {
final Game game = ai.getGame();
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (!sa.usesTargeting()) {
if (sa.hasParam("AllValid")) {
CardCollectionView tgtCards = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);

View File

@@ -103,7 +103,7 @@ public class CounterAi extends SpellAbilityAi {
int toPay = 0;
boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
} else {
@@ -123,7 +123,7 @@ public class CounterAi extends SpellAbilityAi {
}
if (setPayX) {
source.setSVar("PayX", Integer.toString(toPay));
sa.setSVar("PayX", Integer.toString(toPay));
}
}
@@ -267,7 +267,7 @@ public class CounterAi extends SpellAbilityAi {
int toPay = 0;
boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else {
@@ -289,7 +289,7 @@ public class CounterAi extends SpellAbilityAi {
}
if (setPayX) {
source.setSVar("PayX", Integer.toString(toPay));
sa.setSVar("PayX", Integer.toString(toPay));
}
}
}

View File

@@ -338,7 +338,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
if (amountStr.equals("X")) {
if (source.getSVar(amountStr).equals("Count$xPaid")) {
if (sa.getSVar(amountStr).equals("Count$xPaid")) {
// By default, set PayX here to maximum value (used for most SAs of this type).
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
@@ -359,7 +359,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
}
source.setSVar("PayX", Integer.toString(amount));
sa.setSVar("PayX", Integer.toString(amount));
} else if ("ExiledCreatureFromGraveCMC".equals(logic)) {
// e.g. Necropolis
amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), CardPredicates.Accessors.fnGetCmc);
@@ -698,8 +698,8 @@ public class CountersPutAi extends SpellAbilityAi {
list = new CardCollection(AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa));
if (amountStr.equals("X")
&& !source.hasSVar("PayX") /* SubAbility on something that already had set PayX, e.g. Endless One ETB counters */
&& ((sa.hasParam(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")) || source.getSVar(amountStr).equals("Count$xPaid") )) {
&& !sa.hasSVar("PayX") /* SubAbility on something that already had set PayX, e.g. Endless One ETB counters */
&& ((sa.hasParam(amountStr) && sa.getSVar(amountStr).equals("Count$xPaid")))) {
// detect if there's more than one X in the cost (Hangarback Walker, Walking Ballista, etc.)
SpellAbility testSa = sa;
@@ -725,7 +725,7 @@ public class CountersPutAi extends SpellAbilityAi {
// Account for the multiple X in cost
if (countX > 1) { payX /= countX; }
source.setSVar("PayX", Integer.toString(payX));
sa.setSVar("PayX", Integer.toString(payX));
}
if (!mandatory) {
@@ -1031,7 +1031,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
} else {
for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, c) && !ComputerUtil.isUselessCounter(type)) {
if (!ComputerUtil.isNegativeCounter(type, c) && !ComputerUtil.isUselessCounter(type, c)) {
return type;
}
}

View File

@@ -82,10 +82,10 @@ public class CountersPutAllAi extends SpellAbilityAi {
// TODO improve X value to don't overpay when extra mana won't do
// anything more useful
final int amount;
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(amount));
sa.setSVar("PayX", Integer.toString(amount));
} else {
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
}

View File

@@ -157,7 +157,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
if (!ComputerUtil.isNegativeCounter(aType, best)) {
sa.getTargets().add(best);
return true;
} else if (!ComputerUtil.isUselessCounter(aType)) {
} else if (!ComputerUtil.isUselessCounter(aType, best)) {
// whould remove positive counter
if (best.getCounters(aType) <= amount) {
sa.getTargets().add(best);

View File

@@ -143,7 +143,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// variable amount for Hex Parasite
int amount;
boolean xPay = false;
if (amountStr.equals("X") && source.getSVar("X").equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar("X").equals("Count$xPaid")) {
final int manaLeft = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (manaLeft == 0) {
@@ -167,7 +167,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (amount >= ice) {
sa.getTargets().add(depth);
if (xPay) {
source.setSVar("PayX", Integer.toString(ice));
sa.setSVar("PayX", Integer.toString(ice));
}
return true;
}
@@ -186,7 +186,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
Card best = ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList);
sa.getTargets().add(best);
if (xPay) {
source.setSVar("PayX", Integer.toString(best.getCurrentLoyalty()));
sa.setSVar("PayX", Integer.toString(best.getCurrentLoyalty()));
}
return true;
}
@@ -297,7 +297,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
int amount;
boolean xPay = false;
// Timecrafting has X R
if (amountStr.equals("X") && source.getSVar("X").equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar("X").equals("Count$xPaid")) {
final int manaLeft = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (manaLeft == 0) {
@@ -317,7 +317,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
int timeCount = best.getCounters(CounterEnumType.TIME);
sa.getTargets().add(best);
if (xPay) {
source.setSVar("PayX", Integer.toString(timeCount));
sa.setSVar("PayX", Integer.toString(timeCount));
}
return true;
}

View File

@@ -83,7 +83,7 @@ public class DamageAllAi extends SpellAbilityAi {
if (best_x > 0) {
if (sa.getSVar(damage).equals("Count$xPaid")) {
source.setSVar("PayX", Integer.toString(best_x));
sa.setSVar("PayX", Integer.toString(best_x));
}
if (damage.equals("ChosenX")) {
source.setSVar("ChosenX", "Number$" + best_x);
@@ -203,7 +203,7 @@ public class DamageAllAi extends SpellAbilityAi {
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
// Set PayX here to maximum value.
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
}
if (sa.hasParam("ValidPlayers")) {
@@ -285,7 +285,7 @@ public class DamageAllAi extends SpellAbilityAi {
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
// Set PayX here to maximum value.
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
}
if (sa.hasParam("ValidPlayers")) {

View File

@@ -12,7 +12,6 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
@@ -75,8 +74,8 @@ public class DamageDealAi extends DamageAiBase {
}
// Set PayX here to maximum value.
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setSVar("PayX", Integer.toString(dmg));
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.isInZone(ZoneType.Hand)) {
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
}
@@ -96,7 +95,7 @@ public class DamageDealAi extends DamageAiBase {
if (damage.equals("X")) {
if (sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
// Try not to waste spells like Blaze or Fireball on early targets, try to do more damage with them if possible
if (ai.getController().isAI()) {
@@ -113,7 +112,7 @@ public class DamageDealAi extends DamageAiBase {
}
// Set PayX here to maximum value. It will be adjusted later depending on the target.
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
} else if (sa.getSVar(damage).contains("InYourHand") && source.isInZone(ZoneType.Hand)) {
dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
@@ -225,7 +224,7 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) {
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) {
return false;
}
@@ -266,7 +265,7 @@ public class DamageDealAi extends DamageAiBase {
}
}
if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) ||
if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) ||
sourceName.equals("Crater's Claws")){
// If I can kill my target by paying less mana, do it
if (sa.usesTargeting() && !sa.getTargets().isTargetingAnyPlayer() && !sa.hasParam("DividedAsYouChoose")) {
@@ -281,25 +280,13 @@ public class DamageDealAi extends DamageAiBase {
if (sourceName.equals("Crater's Claws") && ai.hasFerocious()) {
actualPay = actualPay > 2 ? actualPay - 2 : 0;
}
source.setSVar("PayX", Integer.toString(actualPay));
}
}
if ("XCountersDamage".equals(logic)) {
// Check to ensure that we have enough counters to remove per the defined PayX
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
if (source.getCounters(((CostRemoveCounter) part).counter) < Integer.valueOf(source.getSVar("PayX"))) {
return false;
}
break;
}
sa.setSVar("PayX", Integer.toString(actualPay));
}
}
if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
final int CMC = Integer.parseInt(source.getSVar("PayX"));
return !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(CMC)).isEmpty();
final int cmc = Integer.parseInt(sa.getSVar("PayX"));
return !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc)).isEmpty();
}
return true;
@@ -990,7 +977,7 @@ public class DamageDealAi extends DamageAiBase {
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
// Set PayX here to maximum value.
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -1002,7 +989,7 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
if (damage.equals("X") && source.getSVar(damage).equals("Count$xPaid") && !sa.hasParam("DividedAsYouChoose")) {
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.hasParam("DividedAsYouChoose")) {
// If I can kill my target by paying less mana, do it
int actualPay = 0;
final boolean noPrevention = sa.hasParam("NoPrevention");
@@ -1018,7 +1005,7 @@ public class DamageDealAi extends DamageAiBase {
}
}
source.setSVar("PayX", Integer.toString(actualPay));
sa.setSVar("PayX", Integer.toString(actualPay));
}
}
@@ -1078,7 +1065,7 @@ public class DamageDealAi extends DamageAiBase {
saTgt.resetTargets();
saTgt.getTargets().add(tgtCreature != null && dmg < opponent.getLife() ? tgtCreature : opponent);
source.setSVar("PayX", Integer.toString(dmg));
sa.setSVar("PayX", Integer.toString(dmg));
return true;
}

View File

@@ -46,7 +46,7 @@ public class DamagePreventAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard)) {
if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard, sa)) {
return false;
}

View File

@@ -34,7 +34,7 @@ public class DamagePreventAllAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard)) {
if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard, sa)) {
return false;
}

View File

@@ -50,7 +50,7 @@ public class DebuffAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source)) {
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
return false;
}
@@ -67,7 +67,7 @@ public class DebuffAi extends SpellAbilityAi {
}
}
if (!sa.usesTargeting() || !sa.getTargetRestrictions().doesTarget()) {
if (!sa.usesTargeting()) {
List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
@@ -93,7 +93,7 @@ public class DebuffAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (!sa.usesTargeting()) {
// TODO - copied from AF_Pump.pumpDrawbackAI() - what should be
// here?
} else {

View File

@@ -48,7 +48,7 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
hasXCost = abCost.getCostMana() != null && abCost.getCostMana().getAmountOfX() > 0;
hasXCost = sa.costHasManaX();
}
if ("AtOpponentsCombatOrAfter".equals(sa.getParam("AILogic"))) {

View File

@@ -80,10 +80,10 @@ public class DestroyAllAi extends SpellAbilityAi {
valid = sa.getParam("ValidCards");
}
if (valid.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
valid = valid.replace("X", Integer.toString(xPay));
}

View File

@@ -75,9 +75,9 @@ public class DigAi extends SpellAbilityAi {
final String num = sa.getParam("DigNum");
final boolean payXLogic = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("PayX");
if (num != null && (num.equals("X") && host.getSVar(num).equals("Count$xPaid")) || payXLogic) {
if (num != null && (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) || payXLogic) {
// By default, set PayX here to maximum value.
if (!(sa instanceof AbilitySub) || host.getSVar("PayX").equals("")) {
if (!(sa instanceof AbilitySub) || sa.getSVar("PayX").equals("")) {
int manaToSave = 0;
// Special logic that asks the AI to conserve a certain amount of mana when paying X
@@ -89,7 +89,7 @@ public class DigAi extends SpellAbilityAi {
if (numCards <= 0) {
return false;
}
host.setSVar("PayX", Integer.toString(numCards));
sa.setSVar("PayX", Integer.toString(numCards));
}
}

View File

@@ -76,14 +76,14 @@ public class DigUntilAi extends SpellAbilityAi {
}
final String num = sa.getParam("Amount");
if ((num != null) && num.equals("X") && source.getSVar(num).equals("Count$xPaid")) {
if ((num != null) && num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) {
// Set PayX here to maximum value.
if (!(sa instanceof AbilitySub) || source.getSVar("PayX").equals("")) {
if (!(sa instanceof AbilitySub) || sa.getSVar("PayX").equals("")) {
int numCards = ComputerUtilMana.determineLeftoverMana(sa, ai);
if (numCards <= 0) {
return false;
}
source.setSVar("PayX", Integer.toString(numCards));
sa.setSVar("PayX", Integer.toString(numCards));
}
}

View File

@@ -41,7 +41,7 @@ public class DiscardAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source)) {
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) {
return false;
}
@@ -85,14 +85,14 @@ public class DiscardAi extends SpellAbilityAi {
}
if (sa.hasParam("NumCards")) {
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
if (sa.getParam("NumCards").equals("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getWeakestOpponent()
.getCardsIn(ZoneType.Hand).size());
if (cardsToDiscard < 1) {
return false;
}
source.setSVar("PayX", Integer.toString(cardsToDiscard));
sa.setSVar("PayX", Integer.toString(cardsToDiscard));
} else {
if (AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCards"), sa) < 1) {
return false;

View File

@@ -238,17 +238,17 @@ public class DrawAi extends SpellAbilityAi {
boolean xPaid = false;
final String num = sa.getParam("NumCards");
if (num != null && num.equals("X")) {
if (source.getSVar(num).equals("Count$xPaid")) {
if (sa.getSVar(num).equals("Count$xPaid")) {
// Set PayX here to maximum value.
if (drawback && !source.getSVar("PayX").equals("")) {
numCards = Integer.parseInt(source.getSVar("PayX"));
if (drawback && !sa.getSVar("PayX").equals("")) {
numCards = Integer.parseInt(sa.getSVar("PayX"));
} else {
numCards = ComputerUtilMana.determineLeftoverMana(sa, ai);
// try not to overdraw
int safeDraw = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
if (sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) { safeDraw++; } // card will be spent
numCards = Math.min(numCards, safeDraw);
source.setSVar("PayX", Integer.toString(numCards));
sa.setSVar("PayX", Integer.toString(numCards));
assumeSafeX = true;
}
xPaid = true;
@@ -340,7 +340,7 @@ public class DrawAi extends SpellAbilityAi {
// for drawing and losing life
if (numCards >= oppA.getLife()) {
if (xPaid) {
source.setSVar("PayX", Integer.toString(oppA.getLife()));
sa.setSVar("PayX", Integer.toString(oppA.getLife()));
}
sa.getTargets().add(oppA);
return true;
@@ -400,7 +400,7 @@ public class DrawAi extends SpellAbilityAi {
}
if (xPaid) {
source.setSVar("PayX", Integer.toString(numCards));
sa.setSVar("PayX", Integer.toString(numCards));
}
}
@@ -411,7 +411,7 @@ public class DrawAi extends SpellAbilityAi {
if (sa.getHostCard().isInZone(ZoneType.Hand)) {
numCards++; // the card will be spent
}
source.setSVar("PayX", Integer.toString(numCards));
sa.setSVar("PayX", Integer.toString(numCards));
} else {
// Don't draw too many cards and then risk discarding
// cards at EOT

View File

@@ -49,7 +49,7 @@ public class LifeGainAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source)) {
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
return false;
}
} else {
@@ -125,10 +125,10 @@ public class LifeGainAi extends SpellAbilityAi {
final String amountStr = sa.getParam("LifeAmount");
int lifeAmount = 0;
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
lifeAmount = xPay;
} else {
lifeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
@@ -213,12 +213,11 @@ public class LifeGainAi extends SpellAbilityAi {
}
}
final Card source = sa.getHostCard();
final String amountStr = sa.getParam("LifeAmount");
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
}
return true;

View File

@@ -34,14 +34,14 @@ public class LifeLoseAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
final String amountStr = sa.getParam("LifeAmount");
int amount = 0;
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// something already set PayX
if (source.hasSVar("PayX")) {
amount = Integer.parseInt(source.getSVar("PayX"));
if (sa.hasSVar("PayX")) {
amount = Integer.parseInt(sa.getSVar("PayX"));
} else {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
amount = xPay;
}
} else {
@@ -72,10 +72,9 @@ public class LifeLoseAi extends SpellAbilityAi {
final String amountStr = sa.getParam("LifeAmount");
int amount = 0;
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
// source.setSVar("PayX", Integer.toString(amount));
} else {
amount = AbilityUtils.calculateAmount(source, amountStr, sa);
}
@@ -101,10 +100,10 @@ public class LifeLoseAi extends SpellAbilityAi {
final String amountStr = sa.getParam("LifeAmount");
int amount = 0;
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(amount));
sa.setSVar("PayX", Integer.toString(amount));
} else {
amount = AbilityUtils.calculateAmount(source, amountStr, sa);
}
@@ -172,10 +171,10 @@ public class LifeLoseAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
final String amountStr = sa.getParam("LifeAmount");
int amount = 0;
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
amount = xPay;
} else {
amount = AbilityUtils.calculateAmount(source, amountStr, sa);

View File

@@ -16,8 +16,6 @@ public class LifeSetAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// Ability_Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final int myLife = ai.getLife();
final Player opponent = ai.getWeakestOpponent();
final int hlife = opponent.getLife();
@@ -42,10 +40,10 @@ public class LifeSetAi extends SpellAbilityAi {
// would be paid
int amount;
// we shouldn't have to worry too much about PayX for SetLife
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
amount = xPay;
} else {
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
@@ -114,10 +112,10 @@ public class LifeSetAi extends SpellAbilityAi {
final String amountStr = sa.getParam("LifeAmount");
int amount;
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
amount = xPay;
} else {
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);

View File

@@ -80,7 +80,7 @@ public class ManifestAi extends SpellAbilityAi {
// Handle either Manifest X cards, or Manifest 1 card and give it X P1P1s
// Set PayX here to maximum value.
int x = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(x));
sa.setSVar("PayX", Integer.toString(x));
if (x <= 0) {
return false;
}

View File

@@ -75,7 +75,6 @@ public class MillAi extends SpellAbilityAi {
* - check for Laboratory Maniac effect (needs to check for actual
* effect due to possibility of "lose abilities" effect)
*/
final Card source = sa.getHostCard();
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevents mill 0 infinite loop?
}
@@ -90,10 +89,10 @@ public class MillAi extends SpellAbilityAi {
}
if ((sa.getParam("NumCards").equals("X") || sa.getParam("NumCards").equals("Z"))
&& source.getSVar("X").startsWith("Count$xPaid")) {
&& sa.getSVar("X").startsWith("Count$xPaid")) {
// Set PayX here to maximum value.
final int cardsToDiscard = getNumToDiscard(ai, sa);
source.setSVar("PayX", Integer.toString(cardsToDiscard));
sa.setSVar("PayX", Integer.toString(cardsToDiscard));
return cardsToDiscard > 0;
}
return true;
@@ -182,11 +181,10 @@ public class MillAi extends SpellAbilityAi {
return false;
}
final Card source = sa.getHostCard();
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
if (sa.getParam("NumCards").equals("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int cardsToDiscard = getNumToDiscard(aiPlayer, sa);
source.setSVar("PayX", Integer.toString(cardsToDiscard));
sa.setSVar("PayX", Integer.toString(cardsToDiscard));
}
return true;

View File

@@ -164,7 +164,7 @@ public class ProtectAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (!sa.usesTargeting()) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (cards.size() == 0) {
return false;
@@ -359,12 +359,7 @@ public class ProtectAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final Card host = sa.getHostCard();
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (host.isCreature()) {
// TODO
}
} else {
if (sa.usesTargeting()) {
return protectTgtAI(ai, sa, false);
}

View File

@@ -33,7 +33,7 @@ public class ProtectAllAi extends SpellAbilityAi {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard)) {
if (!ComputerUtilCost.checkRemoveCounterCost(cost, hostCard, sa)) {
return false;
}

View File

@@ -10,8 +10,6 @@ import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostTapType;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
@@ -288,19 +286,19 @@ public class PumpAi extends PumpAiBase {
}
}
if (source.getSVar("X").equals("Count$xPaid")) {
source.setSVar("PayX", "");
if (sa.getSVar("X").equals("Count$xPaid")) {
sa.setSVar("PayX", "");
}
int defense;
if (numDefense.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (numDefense.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
if (sourceName.equals("Necropolis Fiend")) {
xPay = Math.min(xPay, sa.getActivatingPlayer().getCardsIn(ZoneType.Graveyard).size());
sa.setSVar("X", Integer.toString(xPay));
}
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
defense = xPay;
if (numDefense.equals("-X")) {
defense = -xPay;
@@ -313,13 +311,13 @@ public class PumpAi extends PumpAiBase {
}
int attack;
if (numAttack.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (numAttack.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final String toPay = source.getSVar("PayX");
final String toPay = sa.getSVar("PayX");
if (toPay.equals("")) {
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setSVar("PayX", Integer.toString(xPay));
attack = xPay;
} else {
attack = Integer.parseInt(toPay);
@@ -353,7 +351,7 @@ public class PumpAi extends PumpAiBase {
}
//Untargeted
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (!sa.usesTargeting()) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (cards.isEmpty()) {
@@ -398,19 +396,10 @@ public class PumpAi extends PumpAiBase {
return false;
}
if ("DebuffForXCounters".equals(sa.getParam("AILogic")) && sa.getTargetCard() != null) {
// e.g. Skullmane Baku
CounterType ctrType = CounterType.get(CounterEnumType.KI);
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter;
break;
}
}
if (!sa.getSVar("PayX").isEmpty() && sa.getTargetCard() != null) {
// Do not pay more counters than necessary to kill the targeted creature
int chosenX = Math.min(source.getCounters(ctrType), sa.getTargetCard().getNetToughness());
sa.setSVar("ChosenX", String.valueOf(chosenX));
int chosenX = Math.min(sa.getSVarInt("PayX"), sa.getTargetCard().getNetToughness());
sa.setSVar("PayX", String.valueOf(chosenX));
}
return true;
@@ -573,12 +562,12 @@ public class PumpAi extends PumpAiBase {
}
}
while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) {
while (sa.canAddMoreTarget()) {
Card t = null;
// boolean goodt = false;
if (list.isEmpty()) {
if (sa.getTargets().size() < tgt.getMinTargets(source, sa) || sa.getTargets().size() == 0) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (mandatory || ComputerUtil.activateForCost(sa, ai)) {
return pumpMandatoryTarget(ai, sa);
}
@@ -678,28 +667,27 @@ public class PumpAi extends PumpAiBase {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final String numDefense = sa.hasParam("NumDef") ? sa.getParam("NumDef") : "";
final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : "";
int defense;
if (numDefense.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (numDefense.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setSVar("PayX", Integer.toString(xPay));
defense = xPay;
} else {
defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa);
}
int attack;
if (numAttack.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (numAttack.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final String toPay = source.getSVar("PayX");
final String toPay = sa.getSVar("PayX");
if (toPay.equals("")) {
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setSVar("PayX", Integer.toString(xPay));
attack = xPay;
} else {
attack = Integer.parseInt(toPay);
@@ -708,7 +696,7 @@ public class PumpAi extends PumpAiBase {
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
}
if (sa.getTargetRestrictions() == null) {
if (!sa.usesTargeting()) {
if (mandatory) {
return true;
}
@@ -749,28 +737,28 @@ public class PumpAi extends PumpAiBase {
return false;
}
int defense;
if (numDefense.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
defense = Integer.parseInt(source.getSVar("PayX"));
} else {
defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa);
}
int attack;
if (numAttack.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (source.getSVar("PayX").equals("")) {
if (numAttack.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
if (sa.getSVar("PayX").equals("")) {
// X is not set yet
final int xPay = ComputerUtilMana.determineLeftoverMana(sa.getRootAbility(), ai);
source.setSVar("PayX", Integer.toString(xPay));
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setSVar("PayX", Integer.toString(xPay));
attack = xPay;
} else {
attack = Integer.parseInt(source.getSVar("PayX"));
attack = Integer.parseInt(sa.getSVar("PayX"));
}
} else {
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
}
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
int defense;
if (numDefense.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
defense = Integer.parseInt(sa.getSVar("PayX"));
} else {
defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa);
}
if (!sa.usesTargeting()) {
if (source.isCreature()) {
if (!source.hasKeyword(Keyword.INDESTRUCTIBLE) && source.getNetToughness() + defense <= source.getDamage()) {
return false;

View File

@@ -2,22 +2,18 @@ package forge.ai.ability;
import forge.ai.*;
import forge.game.card.Card;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
public class RepeatAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = ai.getWeakestOpponent();
if (tgt != null) {
if (sa.usesTargeting()) {
if (!opp.canBeTargetedBy(sa)) {
return false;
}
@@ -31,7 +27,7 @@ public class RepeatAi extends SpellAbilityAi {
}
// Set PayX here to maximum value.
final int max = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(max));
sa.setSVar("PayX", Integer.toString(max));
return max > 0;
}
return true;

View File

@@ -103,10 +103,10 @@ public class SacrificeAi extends SpellAbilityAi {
return false;
}
if (num.equals("X") && source.getSVar(num).equals("Count$xPaid")) {
if (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), amount);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
}
final int half = (amount / 2) + (amount % 2); // Half of amount
@@ -135,7 +135,7 @@ public class SacrificeAi extends SpellAbilityAi {
final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1";
int amount = AbilityUtils.calculateAmount(source, num, sa);
if (num.equals("X") && source.getSVar(num).equals("Count$xPaid")) {
if (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) {
// Set PayX here to maximum value.
amount = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), amount);
}

View File

@@ -28,10 +28,10 @@ public class SacrificeAllAi extends SpellAbilityAi {
valid = sa.getParam("ValidCards");
}
if (valid.contains("X") && source.getSVar("X").equals("Count$xPaid")) {
if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay));
}

View File

@@ -32,7 +32,7 @@ public class StoreSVarAi extends SpellAbilityAi {
// Set PayX here to half the remaining mana to allow for Main 2 and other combat shenanigans.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai) / 2;
if (xPay == 0) { return false; }
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
}
final String logic = sa.getParam("AILogic");

View File

@@ -3,18 +3,11 @@ package forge.ai.ability;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import java.util.List;
public class TapAi extends TapAiBase {
@Override
@@ -48,7 +41,6 @@ public class TapAi extends TapAiBase {
return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
final Cost abCost = sa.getPayCosts();
if (abCost != null) {
@@ -57,32 +49,27 @@ public class TapAi extends TapAiBase {
}
}
if (tgt == null) {
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if (!sa.usesTargeting()) {
boolean bFlag = false;
for (final Card c : defined) {
for (final Card c : AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)) {
bFlag |= c.isUntapped();
}
return bFlag;
} else {
if ("TapForXCounters".equals(sa.getParam("AILogic"))) {
// e.g. Waxmane Baku
CounterType ctrType = CounterType.get(CounterEnumType.KI);
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter;
break;
}
}
// X controls the minimum targets
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setSVar("PayX", Integer.toString(xPay));
int numTargetable = Math.min(sa.getHostCard().getCounters(ctrType), ai.getOpponents().getCreaturesInPlay().size());
sa.setSVar("ChosenX", String.valueOf(numTargetable));
// TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore?
sa.setXManaCostPaid(xPay);
// TODO since change of PayX. the shouldCastLessThanMax logic might be faulty
}
sa.resetTargets();
return tapPrefTargeting(ai, source, tgt, sa, false);
return tapPrefTargeting(ai, source, sa, false);
}
}

View File

@@ -18,12 +18,11 @@ import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.List;
public abstract class TapAiBase extends SpellAbilityAi {
public abstract class TapAiBase extends SpellAbilityAi {
/**
* <p>
@@ -41,21 +40,18 @@ public abstract class TapAiBase extends SpellAbilityAi {
*/
private boolean tapTargetList(final Player ai, final SpellAbility sa, final CardCollection tapList, final boolean mandatory) {
final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions();
for (final Card c : sa.getTargets().getTargetCards()) {
tapList.remove(c);
}
tapList.removeAll(sa.getTargets().getTargetCards());
if (tapList.isEmpty()) {
return false;
}
while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) {
while (sa.canAddMoreTarget()) {
Card choice = null;
if (tapList.size() == 0) {
if (sa.getTargets().size() < tgt.getMinTargets(source, sa) || sa.getTargets().size() == 0) {
if (tapList.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (!mandatory) {
sa.resetTargets();
}
@@ -76,7 +72,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
}
if (choice == null) { // can't find anything left
if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().size() == 0) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (!mandatory) {
sa.resetTargets();
}
@@ -111,11 +107,10 @@ public abstract class TapAiBase extends SpellAbilityAi {
* a boolean.
* @return a boolean.
*/
protected boolean tapPrefTargeting(final Player ai, final Card source, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) {
protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
final Player opp = ai.getWeakestOpponent();
final Game game = ai.getGame();
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa);
tapList = CardLists.getTargetableCards(tapList, sa);
tapList = CardLists.filter(tapList, Presets.UNTAPPED);
tapList = CardLists.filter(tapList, new Predicate<Card>() {
@@ -136,10 +131,9 @@ public abstract class TapAiBase extends SpellAbilityAi {
//use broader approach when the cost is a positive thing
if (tapList.isEmpty() && ComputerUtil.activateForCost(sa, ai)) {
tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa);
tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tapList = CardLists.getTargetableCards(tapList, sa);
tapList = CardLists.filter(tapList, new Predicate<Card>() {
tapList = CardLists.filter(tapList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
if (c.isCreature()) {
@@ -162,15 +156,15 @@ public abstract class TapAiBase extends SpellAbilityAi {
tapList.removeAll(toExclude);
if (tapList.isEmpty()) {
return false;
return false;
}
boolean goodTargets = false;
while (sa.getTargets().size() < tgt.getMaxTargets(source, sa)) {
while (sa.canAddMoreTarget()) {
Card choice = null;
if (tapList.isEmpty()) {
if (sa.getTargets().size() < tgt.getMinTargets(source, sa) || sa.getTargets().size() == 0) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (!mandatory) {
sa.resetTargets();
}
@@ -186,8 +180,8 @@ public abstract class TapAiBase extends SpellAbilityAi {
PhaseHandler phase = game.getPhaseHandler();
Card primeTarget = ComputerUtil.getKilledByTargeting(sa, tapList);
if (primeTarget != null) {
choice = primeTarget;
goodTargets = true;
choice = primeTarget;
goodTargets = true;
} else if (phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Tap creatures possible blockers before combat during AI's turn.
List<Card> attackers;
@@ -231,7 +225,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
}
if (choice == null) { // can't find anything left
if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().size() == 0) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
if (!mandatory) {
sa.resetTargets();
}
@@ -265,11 +259,9 @@ public abstract class TapAiBase extends SpellAbilityAi {
*/
protected boolean tapUnpreferredTargeting(final Player ai, final SpellAbility sa, final boolean mandatory) {
final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = ai.getGame();
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), source.getController(), source, sa);
list = CardLists.getTargetableCards(list, sa);
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
// try to tap anything controlled by the computer
CardCollection tapList = CardLists.filterControlledBy(list, ai.getOpponents());
@@ -277,7 +269,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
return true;
}
if (sa.getTargets().size() >= tgt.getMinTargets(sa.getHostCard(), sa)) {
if (sa.isMinTargetChosen()) {
return true;
}
@@ -296,7 +288,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
return true;
}
if (sa.getTargets().size() >= tgt.getMinTargets(sa.getHostCard(), sa)) {
if (sa.isMinTargetChosen()) {
return true;
}
@@ -308,11 +300,9 @@ public abstract class TapAiBase extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
if (tgt == null) {
if (!sa.usesTargeting()) {
if (mandatory) {
return true;
}
@@ -322,7 +312,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
return true;
} else {
sa.resetTargets();
if (tapPrefTargeting(ai, source, tgt, sa, mandatory)) {
if (tapPrefTargeting(ai, source, sa, mandatory)) {
return true;
} else if (mandatory) {
// not enough preferred targets, but mandatory so keep going:
@@ -335,17 +325,14 @@ public abstract class TapAiBase extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
boolean randomReturn = true;
if (tgt == null) {
// either self or defined, either way should be fine
} else {
if (sa.usesTargeting()) {
// target section, maybe pull this out?
sa.resetTargets();
if (!tapPrefTargeting(ai, source, tgt, sa, false)) {
if (!tapPrefTargeting(ai, source, sa, false)) {
return false;
}
}

View File

@@ -4,11 +4,8 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.util.MyRandom;
import java.util.List;
public class TapOrUntapAi extends TapAiBase {
/* (non-Javadoc)
@@ -16,19 +13,17 @@ public class TapOrUntapAi extends TapAiBase {
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
if (tgt == null) {
if (!sa.usesTargeting()) {
// assume we are looking to tap human's stuff
// TODO - check for things with untap abilities, and don't tap
// those.
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
for (final Card c : defined) {
for (final Card c : AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)) {
bFlag |= c.isUntapped();
}
@@ -37,7 +32,7 @@ public class TapOrUntapAi extends TapAiBase {
}
} else {
sa.resetTargets();
if (!tapPrefTargeting(ai, source, tgt, sa, false)) {
if (!tapPrefTargeting(ai, source, sa, false)) {
return false;
}
}

View File

@@ -85,10 +85,10 @@ public class TokenAi extends SpellAbilityAi {
if (source.getSVar("X").equals("Count$Converge")) {
x = ComputerUtilMana.getConvergeCount(sa, ai);
}
if (source.getSVar("X").equals("Count$xPaid")) {
if (sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
x = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(x));
sa.setSVar("PayX", Integer.toString(x));
}
if (x <= 0) {
return false; // 0 tokens or 0 toughness token(s)
@@ -261,10 +261,10 @@ public class TokenAi extends SpellAbilityAi {
if ("X".equals(tokenAmount) || "X".equals(tokenPower) || "X".equals(tokenToughness)) {
int x = AbilityUtils.calculateAmount(source, tokenAmount, sa);
if (source.getSVar("X").equals("Count$xPaid")) {
if (sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
x = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(x));
sa.setSVar("PayX", Integer.toString(x));
}
if (x <= 0) {
return false;

View File

@@ -46,7 +46,7 @@ public class UnattachAllAi extends SpellAbilityAi {
return false;
}
source.setSVar("PayX", Integer.toString(xPay));
sa.setSVar("PayX", Integer.toString(xPay));
}
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)

View File

@@ -51,27 +51,24 @@ public class UntapAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if (tgt == null) {
if (!sa.usesTargeting()) {
final List<Card> pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai;
} else {
return untapPrefTargeting(ai, tgt, sa, false);
return untapPrefTargeting(ai, sa, false);
}
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
if (!sa.usesTargeting()) {
if (mandatory) {
return true;
}
@@ -80,7 +77,7 @@ public class UntapAi extends SpellAbilityAi {
final List<Card> pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai;
} else {
if (untapPrefTargeting(ai, tgt, sa, mandatory)) {
if (untapPrefTargeting(ai, sa, mandatory)) {
return true;
} else if (mandatory) {
// not enough preferred targets, but mandatory so keep going:
@@ -100,7 +97,7 @@ public class UntapAi extends SpellAbilityAi {
if (tgt == null) {
// who cares if its already untapped, it's only a subability?
} else {
if (!untapPrefTargeting(ai, tgt, sa, false)) {
if (!untapPrefTargeting(ai, sa, false)) {
return false;
}
}
@@ -113,15 +110,13 @@ public class UntapAi extends SpellAbilityAi {
* untapPrefTargeting.
* </p>
*
* @param tgt
* a {@link forge.game.spellability.TargetRestrictions} object.
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @param mandatory
* a boolean.
* @return a boolean.
*/
private static boolean untapPrefTargeting(final Player ai, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) {
private static boolean untapPrefTargeting(final Player ai, final SpellAbility sa, final boolean mandatory) {
final Card source = sa.getHostCard();
Player targetController = ai;
@@ -131,7 +126,6 @@ public class UntapAi extends SpellAbilityAi {
}
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
if (list.isEmpty()) {
return false;
@@ -175,7 +169,7 @@ public class UntapAi extends SpellAbilityAi {
untapList.removeAll(toExclude);
sa.resetTargets();
while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
while (sa.canAddMoreTarget()) {
Card choice = null;
if (untapList.isEmpty()) {
@@ -183,7 +177,7 @@ public class UntapAi extends SpellAbilityAi {
if (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.Animate && !list.isEmpty()
&& ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
choice = ComputerUtilCard.getWorstPermanentAI(list, false, false, false, false);
} else if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().size() == 0) {
} else if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
return false;
} else {
@@ -204,7 +198,7 @@ public class UntapAi extends SpellAbilityAi {
}
if (choice == null) { // can't find anything left
if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa) || sa.getTargets().size() == 0) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
return false;
} else {

View File

@@ -126,7 +126,7 @@ public class ForgeScript {
} else if (property.equals("nonManaAbility")) {
return !sa.isManaAbility();
} else if (property.equals("withoutXCost")) {
return !sa.isXCost();
return !sa.costHasManaX();
} else if (property.equals("Buyback")) {
return sa.isBuyBackAbility();
} else if (property.equals("Cycling")) {

View File

@@ -227,12 +227,6 @@ public class GameAction {
// ensure that any leftover keyword/type changes are cleared in the state view
copied.updateStateForView();
// Clean up temporary variables such as Sunburst value or announced PayX value
if (!(zoneTo.is(ZoneType.Stack) || zoneTo.is(ZoneType.Battlefield))) {
copied.clearTemporaryVars();
}
if (!suppress) {
if (zoneFrom == null) {
copied.getOwner().addInboundToken(copied);

View File

@@ -31,15 +31,43 @@ public class GameEntityCounterTable extends ForwardingTable<GameEntity, CounterT
*/
@Override
public Integer put(GameEntity rowKey, CounterType columnKey, Integer value) {
Integer old = contains(rowKey, columnKey) ? get(rowKey, columnKey) : 0;
return super.put(rowKey, columnKey, old + value);
return super.put(rowKey, columnKey, get(rowKey, columnKey) + value);
}
@Override
public Integer get(Object rowKey, Object columnKey) {
if (!contains(rowKey, columnKey)) {
return 0; // helper to not return null value
}
return super.get(rowKey, columnKey);
}
/*
* returns the counters that can still be removed from game entity
*/
public Map<CounterType, Integer> filterToRemove(GameEntity ge) {
Map<CounterType, Integer> result = Maps.newHashMap();
if (!containsRow(ge)) {
result.putAll(ge.getCounters());
return result;
}
Map<CounterType, Integer> alreadyRemoved = row(ge);
for (Map.Entry<CounterType, Integer> e : ge.getCounters().entrySet()) {
Integer rest = e.getValue() - (alreadyRemoved.containsKey(e.getKey()) ? alreadyRemoved.get(e.getKey()) : 0);
if (rest > 0) {
result.put(e.getKey(), rest);
}
}
return result;
}
public Map<GameEntity, Integer> filterTable(CounterType type, String valid, Card host, SpellAbility sa) {
Map<GameEntity, Integer> result = Maps.newHashMap();
for (Map.Entry<GameEntity, Integer> e : column(type).entrySet()) {
if (e.getKey().isValid(valid, host.getController(), host, sa)) {
if (e.getValue() > 0 && e.getKey().isValid(valid, host.getController(), host, sa)) {
result.put(e.getKey(), e.getValue());
}
}

View File

@@ -6426,17 +6426,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
return changed;
}
public final void clearTemporaryVars() {
// Add cleanup for all variables that are set temporarily but that need
// to be restored to their original value if a card changes zones
removeSVar("PayX"); // Temporary AI X announcement variable
removeSVar("IsCastFromPlayEffect"); // Temporary SVar indicating that the spell is cast indirectly via AF Play
setXManaCostPaidByColor(null);
setKickerMagnitude(0);
setPseudoMultiKickerMagnitude(0);
}
public final int getFinalChapterNr() {
int n = 0;
for (final Trigger t : getTriggers()) {

View File

@@ -51,7 +51,6 @@ import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.zone.Zone;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Expressions;
@@ -227,50 +226,6 @@ public class CardFactoryUtil {
return AbilityFactory.getAbility(ab, sourceCard);
}
/**
* <p>
* isTargetStillValid.
* </p>
*
* @param ability
* a {@link forge.game.spellability.SpellAbility} object.
* @param target
* a {@link forge.game.card.Card} object.
* @return a boolean.
*/
public static boolean isTargetStillValid(final SpellAbility ability, final Card target) {
Zone zone = target.getGame().getZoneOf(target);
if (zone == null) {
return false; // for tokens that disappeared
}
final Card source = ability.getHostCard();
final TargetRestrictions tgt = ability.getTargetRestrictions();
if (tgt != null) {
// Reconfirm the Validity of a TgtValid, or if the Creature is still
// a Creature
if (tgt.doesTarget()
&& !target.isValid(tgt.getValidTgts(), ability.getActivatingPlayer(), ability.getHostCard(), ability)) {
return false;
}
// Check if the target is in the zone it needs to be in to be targeted
if (!tgt.getZone().contains(zone.getZoneType())) {
return false;
}
}
else {
// If an Aura's target is removed before it resolves, the Aura
// fizzles
if (source.isAura() && !target.isInZone(ZoneType.Battlefield)) {
return false;
}
}
// Make sure it's still targetable as well
return ability.canTarget(target);
}
// does "target" have protection from "card"?
/**
* <p>

View File

@@ -1360,7 +1360,7 @@ public class CardProperty {
}
} else if (property.startsWith("hasXCost")) {
SpellAbility sa1 = card.getFirstSpellAbility();
if (sa1 != null && !sa1.isXCost()) {
if (sa1 != null && !sa1.costHasManaX()) {
return false;
}
} else if (property.startsWith("suspended")) {

View File

@@ -45,6 +45,9 @@ public class CounterType implements Comparable<CounterType>, Serializable {
}
public static CounterType getType(String name) {
if ("Any".equalsIgnoreCase(name)) {
return null;
}
try {
return get(CounterEnumType.getType(name));
} catch (final IllegalArgumentException ex) {

View File

@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Lists;
@@ -400,9 +401,9 @@ public class Cost implements Serializable {
}
if (parse.startsWith("RemoveAnyCounter<")) {
final String[] splitStr = abCostParse(parse, 3);
final String description = splitStr.length > 2 ? splitStr[2] : null;
return new CostRemoveAnyCounter(splitStr[0], splitStr[1], description);
final String[] splitStr = abCostParse(parse, 4);
final String description = splitStr.length > 3 ? splitStr[3] : null;
return new CostRemoveAnyCounter(splitStr[0], CounterType.getType(splitStr[1]), splitStr[2], description);
}
if (parse.startsWith("Exile<")) {
@@ -959,5 +960,20 @@ public class Cost implements Serializable {
return xCost;
}
public Integer getMaxForNonManaX(final SpellAbility ability, final Player payer) {
Integer val = null;
for (CostPart p : getCostParts()) {
if (!p.getAmount().equals("X")) {
continue;
}
val = ObjectUtils.min(val, p.getMaxAmountX(ability, payer));
}
// extra 0 check
if (val != null && val <= 0 && hasManaCost() && !getCostMana().canXbe0()) {
val = null;
}
return val;
}
public static final Cost Zero = new Cost(0);
}

View File

@@ -74,6 +74,9 @@ public abstract class CostPart implements Comparable<CostPart>, Cloneable, Seria
return this.amount;
}
public Integer getMaxAmountX(final SpellAbility ability, final Player payer) {
return null;
}
/**
* Gets the type.
*

View File

@@ -47,6 +47,10 @@ public class CostPayEnergy extends CostPart {
@Override
public int paymentOrder() { return 7; }
public Integer getMaxAmountX(final SpellAbility ability, final Player payer) {
return payer.getCounters(CounterEnumType.ENERGY);
}
/*
* (non-Javadoc)
*

View File

@@ -17,19 +17,19 @@
*/
package forge.game.cost;
import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.TextUtil;
import java.util.Map;
import com.google.common.collect.Table;
/**
* The Class CostRemoveAnyCounter.
*/
public class CostRemoveAnyCounter extends CostPartWithList {
public class CostRemoveAnyCounter extends CostPart {
/**
* Serializables need a version ID.
*/
@@ -37,13 +37,8 @@ public class CostRemoveAnyCounter extends CostPartWithList {
// RemoveAnyCounter<Num/Type/{TypeDescription}>
// Power Conduit and Chisei, Heart of Oceans
// Both cards have "Remove a counter from a permanent you control"
private CounterType counterType;
/**
* @param counterType the counterType to set
*/
public void setCounterType(CounterType counterType) {
this.counterType = counterType;
}
public final CounterType counter;
/**
* Instantiates a new cost CostRemoveAnyCounter.
@@ -51,33 +46,34 @@ public class CostRemoveAnyCounter extends CostPartWithList {
* @param amount
* the amount
*/
public CostRemoveAnyCounter(final String amount, final String type, final String description) {
public CostRemoveAnyCounter(final String amount, final CounterType counter, final String type, final String description) {
super(amount, type, description);
this.counter = counter;
}
@Override
public int paymentOrder() { return 8; }
/* (non-Javadoc)
* @see forge.card.cost.CostPartWithList#getHashForList()
*/
@Override
public String getHashForLKIList() {
return "CounterRemove";
}
@Override
public String getHashForCardList() {
return "CounterRemoveCards";
public Integer getMaxAmountX(final SpellAbility ability, final Player payer) {
final Card source = ability.getHostCard();
CardCollectionView validCards = CardLists.getValidCards(payer.getCardsIn(ZoneType.Battlefield), this.getType().split(";"), payer, source, ability);
int allCounters = 0;
for (Card c : validCards) {
if (this.counter != null) {
allCounters += c.getCounters(this.counter);
} else {
for (Integer value : c.getCounters().values()) {
allCounters += value;
}
}
}
return allCounters;
}
/**
* Gets the counter.
*
* @return the counter
*/
public CounterType getCounter() {
return this.counterType;
}
/*
* (non-Javadoc)
@@ -88,27 +84,7 @@ public class CostRemoveAnyCounter extends CostPartWithList {
*/
@Override
public final boolean canPay(final SpellAbility ability, final Player payer) {
final Card source = ability.getHostCard();
CardCollectionView validCards = payer.getCardsIn(ZoneType.Battlefield);
validCards = CardLists.getValidCards(validCards, this.getType().split(";"), payer, source, ability);
validCards = CardLists.filter(validCards, CardPredicates.hasCounters());
if (validCards.isEmpty()) {
return false;
}
Integer i = this.convertAmount();
if (i == null) {
i = AbilityUtils.calculateAmount(source, this.getAmount(), ability);
}
int allCounters = 0;
for (Card c : validCards) {
final Map<CounterType, Integer> tgtCounters = c.getCounters();
for (Integer value : tgtCounters.values()) {
allCounters += value;
}
}
return i <= allCounters;
return AbilityUtils.calculateAmount(ability.getHostCard(), this.getAmount(), ability) <= getMaxAmountX(ability, payer);
}
/*
@@ -120,39 +96,29 @@ public class CostRemoveAnyCounter extends CostPartWithList {
public final String toString() {
final StringBuilder sb = new StringBuilder();
String counters = this.counter == null ? "counter" : this.counter.getName() + " counter";
sb.append("Remove ");
sb.append(Cost.convertIntAndTypeToWords(this.convertAmount(), "counter"));
sb.append(Cost.convertAmountTypeToWords(this.convertAmount(), this.getAmount(), counters));
final String desc = this.getTypeDescription() == null ? this.getType() : this.getTypeDescription();
sb.append(" from ").append(desc);
return sb.toString();
}
@Override
public boolean payAsDecided(Player ai, PaymentDecision decision, SpellAbility ability) {
final String amount = this.getAmount();
final Card source = ability.getHostCard();
Integer c = this.convertAmount();
if (c == null) {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
if (decision.cards.isEmpty()) {
System.err.println(TextUtil.concatWithSpace("Warning: payment decision array was empty when paying CostRemoveAnyCounter for" , ability.getDescription(), "from", ability.getHostCard().toString()));
return false;
}
Card valid = decision.cards.get(0);
counterType = decision.ct;
for (int i = 0; i < c; i++) {
executePayment(ability, valid);
}
source.setSVar("CostCountersRemoved", Integer.toString(c));
return true;
}
@Override
protected Card doPayment(SpellAbility ability, Card targetCard){
targetCard.subtractCounter(this.getCounter(), 1);
return targetCard;
public boolean payAsDecided(Player ai, PaymentDecision decision, SpellAbility ability) {
final Card source = ability.getHostCard();
int removed = 0;
for (Table.Cell<GameEntity, CounterType, Integer> cell : decision.counterTable.cellSet()) {
removed += cell.getValue();
cell.getRowKey().subtractCounter(cell.getColumnKey(), cell.getValue());
}
source.setSVar("CostCountersRemoved", Integer.toString(removed));
return true;
}
public <T> T accept(ICostVisitor<T> visitor) {

View File

@@ -19,6 +19,7 @@ package forge.game.cost;
import com.google.common.collect.Lists;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CounterEnumType;
@@ -32,7 +33,7 @@ import java.util.List;
/**
* The Class CostRemoveCounter.
*/
public class CostRemoveCounter extends CostPartWithList {
public class CostRemoveCounter extends CostPart {
// SubCounter<Num/Counter/{Type/TypeDescription/Zone}>
// Here are the cards that have RemoveCounter<Type>
@@ -47,7 +48,6 @@ public class CostRemoveCounter extends CostPartWithList {
private static final long serialVersionUID = 1L;
public final CounterType counter;
public final ZoneType zone;
private int cntRemoved;
/**
* Instantiates a new cost remove counter.
@@ -73,7 +73,28 @@ public class CostRemoveCounter extends CostPartWithList {
public int paymentOrder() { return 8; }
@Override
public boolean isUndoable() { return true; }
public Integer getMaxAmountX(final SpellAbility ability, final Player payer) {
final CounterType cntrs = this.counter;
final Card source = ability.getHostCard();
final String type = this.getType();
if (this.payCostFromSource()) {
return source.getCounters(cntrs);
} else {
List<Card> typeList;
if (type.equals("OriginalHost")) {
typeList = Lists.newArrayList(ability.getOriginalHost());
} else {
typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability);
}
// Single Target
int maxcount = 0;
for (Card c : typeList) {
maxcount = Math.max(maxcount, c.getCounters(cntrs));
}
return maxcount;
}
}
/*
* (non-Javadoc)
@@ -106,19 +127,6 @@ public class CostRemoveCounter extends CostPartWithList {
return sb.toString();
}
/*
* (non-Javadoc)
*
* @see forge.card.cost.CostPart#refund(forge.Card)
*/
@Override
public final void refund(final Card source) {
int refund = this.getCardList().size() == 1 ? this.cntRemoved : 1; // is wrong for Ooze Flux and Novijen Sages
for (final Card c : this.getCardList()) {
c.addCounter(this.counter, refund, source.getController(), false, false, null);
}
}
/*
* (non-Javadoc)
*
@@ -144,21 +152,10 @@ public class CostRemoveCounter extends CostPartWithList {
typeList = CardLists.getValidCards(payer.getCardsIn(this.zone), type.split(";"), payer, source, ability);
}
if (amount != null) {
// TODO find better way than TypeDescription
if (this.getTypeDescription().equals("among creatures you control")) {
// remove X counters from among creatures you control
int totalCounters = 0;
for (Card c : typeList) {
totalCounters += c.getCounters(cntrs);
}
return totalCounters >= amount;
} else {
// (default logic) remove X counters from a single permanent
for (Card c : typeList) {
if (c.getCounters(cntrs) - amount >= 0) {
return true;
}
// (default logic) remove X counters from a single permanent
for (Card c : typeList) {
if (c.getCounters(cntrs) - amount >= 0) {
return true;
}
}
return false;
@@ -171,32 +168,19 @@ public class CostRemoveCounter extends CostPartWithList {
@Override
public boolean payAsDecided(Player ai, PaymentDecision decision, SpellAbility ability) {
Card source = ability.getHostCard();
cntRemoved = decision.c;
for (final Card card : decision.cards) {
executePayment(ability, card);
int removed = 0;
int toRemove = AbilityUtils.calculateAmount(source, getAmount(), ability);
// for this cost, the list should be only one
for (Card c : decision.cards) {
removed += toRemove;
c.subtractCounter(counter, toRemove);
}
source.setSVar("CostCountersRemoved", Integer.toString(cntRemoved));
source.setSVar("CostCountersRemoved", Integer.toString(removed));
return true;
}
@Override
protected Card doPayment(SpellAbility ability, Card targetCard){
targetCard.subtractCounter(this.counter, cntRemoved);
return targetCard;
}
/* (non-Javadoc)
* @see forge.card.cost.CostPartWithList#getHashForList()
*/
@Override
public String getHashForLKIList() {
return "CounterRemove";
}
@Override
public String getHashForCardList() {
return "CounterRemoveCards";
}
public <T> T accept(ICostVisitor<T> visitor) {
return visitor.visit(this);
}

View File

@@ -1,5 +1,6 @@
package forge.game.cost;
import forge.game.GameEntityCounterTable;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CounterType;
@@ -10,39 +11,41 @@ import forge.util.TextUtil;
import java.util.List;
public class PaymentDecision {
public int c = 0;
public String type;
public CounterType ct;
public final CardCollection cards = new CardCollection();
public final List<Mana> mana;
public final List<Player> players;
public final List<SpellAbility> sp;
// used for CostRemoveAnyCounter
public final GameEntityCounterTable counterTable;
public PaymentDecision(int cnt) {
this(null, null, null, null);
this(null, null, null, null, null);
c = cnt;
}
private PaymentDecision(Iterable<Card> chosen, List<Mana> manaProduced, List<Player> players,
List<SpellAbility> sp) {
List<SpellAbility> sp, GameEntityCounterTable counterTable) {
if (chosen != null) {
cards.addAll(chosen);
}
mana = manaProduced;
this.players = players;
this.sp = sp;
this.counterTable = counterTable;
}
private PaymentDecision(Card chosen) {
this(null, null, null, null);
this(null, null, null, null, null);
cards.add(chosen);
}
public PaymentDecision(String choice) {
this(null, null, null, null);
this(null, null, null, null, null);
type = choice;
}
@@ -62,17 +65,17 @@ public class PaymentDecision {
}
public static PaymentDecision card(Iterable<Card> chosen) {
return new PaymentDecision(chosen, null, null, null);
return new PaymentDecision(chosen, null, null, null, null);
}
public static PaymentDecision card(Iterable<Card> chosen, int n) {
PaymentDecision res = new PaymentDecision(chosen, null, null, null);
PaymentDecision res = new PaymentDecision(chosen, null, null, null, null);
res.c = n;
return res;
}
public static PaymentDecision mana(List<Mana> manas) {
return new PaymentDecision(null, manas, null, null);
return new PaymentDecision(null, manas, null, null, null);
}
@@ -90,16 +93,14 @@ public class PaymentDecision {
public static PaymentDecision players(List<Player> players) {
// TODO Auto-generated method stub
return new PaymentDecision(null, null, players, null);
return new PaymentDecision(null, null, players, null, null);
}
public static PaymentDecision spellabilities(List<SpellAbility> sp) {
return new PaymentDecision(null, null, null, sp);
return new PaymentDecision(null, null, null, sp, null);
}
public static PaymentDecision card(Card selected, CounterType counterType) {
PaymentDecision res = card(selected);
res.ct = counterType;
return res;
public static PaymentDecision counters(GameEntityCounterTable counterTable) {
return new PaymentDecision(null, null, null, null, counterTable);
}
}

View File

@@ -64,9 +64,7 @@ public abstract class AbilityActivated extends SpellAbility implements Cloneable
*/
public AbilityActivated(final Card sourceCard, final Cost abCost, final TargetRestrictions tgt) {
super(sourceCard, abCost);
if ((tgt != null) && tgt.doesTarget()) {
this.setTargetRestrictions(tgt);
}
this.setTargetRestrictions(tgt);
}
public boolean isActivatedAbility() { return true; }

View File

@@ -348,7 +348,7 @@ public class AbilityManaPart implements java.io.Serializable {
}
if (restriction.startsWith("CostContainsX")) {
if (sa.isXCost()) {
if (sa.costHasManaX()) {
return true;
}
continue;

View File

@@ -47,9 +47,7 @@ public abstract class AbilityStatic extends Ability implements Cloneable {
public AbilityStatic(final Card sourceCard, final Cost abCost, final TargetRestrictions tgt) {
super(sourceCard, abCost);
if ((tgt != null) && tgt.doesTarget()) {
this.setTargetRestrictions(tgt);
}
this.setTargetRestrictions(tgt);
}
@Override
public boolean canPlay() {

View File

@@ -39,7 +39,6 @@ import forge.game.card.CardPredicates;
import forge.game.card.CardZoneTable;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
import forge.game.mana.Mana;
@@ -985,7 +984,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
final TargetRestrictions tr = getTargetRestrictions();
// Restriction related to this ability
if (tr != null) {
if (usesTargeting()) {
if (tr.isUniqueTargets() && getUniqueTargets().contains(entity))
return false;
@@ -1322,11 +1321,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
originalMapParams.put("Announce", announce + ";" + variable);
}
public boolean isXCost() {
CostPartMana cm = payCosts != null ? getPayCosts().getCostMana() : null;
return cm != null && cm.getAmountOfX() > 0;
}
@Override
public boolean canBeTargetedBy(SpellAbility sa) {
return sa.canTargetSpellAbility(this);
@@ -1404,14 +1398,22 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
return getTargetRestrictions().getMinTargets(hostCard, this) == 0 && getTargets().size() == 0;
}
public boolean isMinTargetChosen() {
return getTargetRestrictions().isMinTargetsChosen(hostCard, this);
}
public boolean isMaxTargetChosen() {
return getTargetRestrictions().isMaxTargetsChosen(hostCard, this);
}
public boolean isTargetNumberValid() {
if (!this.usesTargeting()) {
return getTargets().isEmpty();
}
int minTargets = getTargetRestrictions().getMinTargets(hostCard, this);
if (!isMinTargetChosen()) {
return false;
}
int maxTargets = getTargetRestrictions().getMaxTargets(hostCard, this);
int numTargets = getTargets().size();
if (maxTargets == 0 && getPayCosts().hasSpecificCostType(CostRemoveCounter.class)
&& hasSVar(getParam("TargetMax"))
@@ -1424,7 +1426,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
maxTargets = Integer.parseInt(getHostCard().getSVar("CostCountersRemoved"));
}
return minTargets <= numTargets && maxTargets >= numTargets;
return maxTargets >= getTargets().size();
}
/**
* <p>
@@ -1665,8 +1667,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
SpellAbility currentAbility = this;
final Card source = getHostCard();
do {
final TargetRestrictions tgt = currentAbility.getTargetRestrictions();
if (tgt != null && tgt.doesTarget()) {
if (currentAbility.usesTargeting()) {
currentAbility.clearTargets();
Player targetingPlayer;
if (currentAbility.hasParam("TargetingPlayer")) {

View File

@@ -24,7 +24,6 @@ import forge.game.GameType;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
import forge.game.phase.PhaseHandler;
@@ -271,7 +270,7 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
if (this.isAllTargetsLegal()) {
for (Card c : sa.getTargets().getTargetCards()) {
if (!CardFactoryUtil.isTargetStillValid(sa, c)) {
if (!sa.canTarget(c)) {
return false;
}
}

View File

@@ -49,7 +49,6 @@ public class TargetRestrictions {
// Target Choices (which is specific for the StackInstance)
// What this Object is restricted to targeting
private boolean tgtValid = false;
private String[] originalValidTgts,
validTgts;
private String uiPrompt = "";
@@ -93,7 +92,6 @@ public class TargetRestrictions {
* a {@link forge.game.spellability.TargetRestrictions} object.
*/
public TargetRestrictions(final TargetRestrictions target) {
this.tgtValid = true;
this.uiPrompt = target.getVTSelection();
this.originalValidTgts = target.getValidTgts();
this.validTgts = this.originalValidTgts.clone();
@@ -129,7 +127,6 @@ public class TargetRestrictions {
* a {@link java.lang.String} object.
*/
public TargetRestrictions(final String prompt, final String[] valid, final String min, final String max) {
this.tgtValid = true;
this.uiPrompt = prompt;
this.originalValidTgts = valid;
this.validTgts = this.originalValidTgts.clone();
@@ -172,17 +169,6 @@ public class TargetRestrictions {
this.maxTotalCMC = cmc;
}
/**
* <p>
* doesTarget.
* </p>
*
* @return a boolean.
*/
public final boolean doesTarget() {
return this.tgtValid;
}
/**
* <p>
* getValidTgts.
@@ -210,7 +196,7 @@ public class TargetRestrictions {
*
* @return the min targets
*/
private final String getMinTargets() {
public final String getMinTargets() {
return this.minTargets;
}
@@ -219,7 +205,7 @@ public class TargetRestrictions {
*
* @return the max targets
*/
private final String getMaxTargets() {
public final String getMaxTargets() {
return this.maxTargets;
}
@@ -278,8 +264,7 @@ public class TargetRestrictions {
* @return a boolean.
*/
public final boolean isMaxTargetsChosen(final Card c, final SpellAbility sa) {
TargetChoices choice = sa.getTargets();
return this.getMaxTargets(c, sa) == choice.size();
return this.getMaxTargets(c, sa) == sa.getTargets().size();
}
/**
@@ -294,11 +279,11 @@ public class TargetRestrictions {
* @return a boolean.
*/
public final boolean isMinTargetsChosen(final Card c, final SpellAbility sa) {
if (this.getMinTargets(c, sa) == 0) {
int min = getMinTargets(c, sa);
if (min == 0) {
return true;
}
TargetChoices choice = sa.getTargets();
return this.getMinTargets(c, sa) <= choice.size();
return min <= sa.getTargets().size();
}
/**

View File

@@ -41,7 +41,6 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardUtil;
import forge.game.event.EventValueChangeType;
import forge.game.event.GameEventCardStatsChanged;
@@ -588,7 +587,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
if (current != null) {
invalidTarget = current.getTimestamp() != card.getTimestamp();
}
invalidTarget |= !(CardFactoryUtil.isTargetStillValid(sa, card));
invalidTarget |= !sa.canTarget(card);
} else {
if (o instanceof SpellAbility) {
SpellAbilityStackInstance si = getInstanceFromSpellAbility((SpellAbility)o);
@@ -616,7 +615,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
}
}
else if (sa.getTargetCard() != null) {
fizzle = !CardFactoryUtil.isTargetStillValid(sa, sa.getTargetCard());
fizzle = !sa.canTarget(sa.getTargetCard());
}
else {
// Set fizzle to the same as the parent if there's no target info

View File

@@ -3,12 +3,10 @@ ManaCost:1 R
Types:Creature Spirit
PT:1/1
T:Mode$ SpellCast | ValidCard$ Spirit,Arcane | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter | TriggerDescription$ Whenever you cast a Spirit or Arcane spell, you may put a ki counter on CARDNAME.
SVar:TrigPutCounter:DB$PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
A:AB$ Pump | Cost$ 1 SubCounter<X/KI> | Defined$ Self | NumAtt$ +Y | References$ X,Y | SpellDescription$ For each counter removed, CARDNAME gets +2/+0 until end of turn.
SVar:X:XChoice
#ChosenX SVar created by Cost payment
SVar:Y:SVar$ChosenX/Times.2
SVar:X:Count$xPaid
SVar:Y:SVar$X/Times.2
AI:RemoveDeck:Random
DeckHints:Type$Spirit|Arcane
SVar:Picture:http://www.wizards.com/global/images/magic/general/blademane_baku.jpg
Oracle:Whenever you cast a Spirit or Arcane spell, you may put a ki counter on Blademane Baku.\n{1}, Remove X ki counters from Blademane Baku: For each counter removed, Blademane Baku gets +2/+0 until end of turn.

View File

@@ -4,7 +4,7 @@ Types:Artifact Creature Construct
PT:0/0
K:etbCounter:P1P1:Y:no Condition:CARDNAME enters the battlefield with a +1/+1 counter on it for each color of mana spent to cast it.
SVar:Y:Count$Converge
A:AB$ DealDamage | Announce$ X | Cost$ X T SubCounter<X/P1P1> | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | NumDmg$ X | References$ X | AILogic$ XCountersDamage | SpellDescription$ CARDNAME deals X damage to any target.
A:AB$ DealDamage | Cost$ X T SubCounter<X/P1P1> | ValidTgts$ Creature,Player,Planeswalker | TgtPrompt$ Select any target | NumDmg$ X | References$ X | SpellDescription$ CARDNAME deals X damage to any target.
SVar:X:Count$xPaid
A:AB$ ChangeZone | Cost$ W U B R G | Origin$ Graveyard | Destination$ Hand | ActivationZone$ Graveyard | SpellDescription$ Return CARDNAME from your graveyard to your hand.
SVar:DiscardMe:1

View File

@@ -4,8 +4,8 @@ Types:Legendary Creature Spirit
PT:4/4
K:Flying
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigSac | TriggerDescription$ At the beginning of your upkeep, sacrifice CARDNAME unless you remove a counter from a permanent you control.
SVar:TrigSac:DB$ Sacrifice | Defined$ Self | UnlessPayer$ You | UnlessCost$ RemoveAnyCounter<1/Permanent.YouCtrl/a permanent you control>
SVar:TrigSac:DB$ Sacrifice | Defined$ Self | UnlessPayer$ You | UnlessCost$ RemoveAnyCounter<1/Any/Permanent.YouCtrl/a permanent you control>
DeckNeeds:Ability$Counters
SVar:NeedsToPlay:Creature.YouCtrl+HasCounters
SVar:Picture:http://www.wizards.com/global/images/magic/general/chisei_heart_of_oceans.jpg
SVar:AIRemoveCounterCostPriority:ANY
Oracle:Flying\nAt the beginning of your upkeep, sacrifice Chisei, Heart of Oceans unless you remove a counter from a permanent you control.

View File

@@ -7,5 +7,4 @@ A:AB$ Draw | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | Defined$ You | N
SVar:X:Count$GreatestPower_Creature.YouCtrl
A:AB$ Token | Cost$ SubCounter<6/LOYALTY> | Planeswalker$ True | Ultimate$ True | TokenAmount$ Y | TokenScript$ g_6_6_wurm | TokenOwner$ You | LegacyImage$ g 6 6 wurm m12 | SpellDescription$ Create a 6/6 green Wurm creature for each land you control.
SVar:Y:Count$Valid Land.YouCtrl
SVar:Picture:http://www.wizards.com/global/images/magic/general/garruk_primal_hunter.jpg
Oracle:[+1]: Create a 3/3 green Beast creature token.\n[-3]: Draw cards equal to the greatest power among creatures you control.\n[-6]: Create a 6/6 green Wurm creature for each land you control.

View File

@@ -6,10 +6,9 @@ A:AB$ GainLife | Cost$ AddCounter<2/LOYALTY> | Planeswalker$ True | LifeAmount$
SVar:GreatestPow:Count$GreatestPower_Creature.YouCtrl
A:AB$ Token | Cost$ AddCounter<0/LOYALTY> | Planeswalker$ True | TokenAmount$ 1 | TokenScript$ g_3_3_dinosaur_trample | TokenOwner$ You | SpellDescription$ Create a 3/3 green Dinosaur creature token with trample.
# TODO: The AI never uses the Ultimate ability (most likely doesn't have the required logic for it)
A:AB$ DealDamage | Cost$ SubCounter<X/LOYALTY> | Announce$ X | XMaxLimit$ L | NumDmg$ X | References$ X,L | Planeswalker$ True | Ultimate$ True | ValidTgts$ Creature | TgtPrompt$ Select any number of target creatures | TargetMin$ 1 | TargetMax$ X | DividedAsYouChoose$ X | RememberDamaged$ True | SubAbility$ DBNoBlock | SpellDescription$ CARDNAME deals X damage divided as you choose among any number of target creatures. Creatures dealt damage this way can't block this turn.
A:AB$ DealDamage | Cost$ SubCounter<X/LOYALTY> | Announce$ X | NumDmg$ X | References$ X | Planeswalker$ True | Ultimate$ True | ValidTgts$ Creature | TgtPrompt$ Select any number of target creatures | TargetMin$ 1 | TargetMax$ X | DividedAsYouChoose$ X | RememberDamaged$ True | SubAbility$ DBNoBlock | SpellDescription$ CARDNAME deals X damage divided as you choose among any number of target creatures. Creatures dealt damage this way can't block this turn.
SVar:DBNoBlock:DB$ Pump | KW$ HIDDEN CARDNAME can't block. | Defined$ Remembered | SubAbility$ DBCleanup | StackDescription$ Creatures dealt damage this way can't block this turn.
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:X:Count$XChoice
SVar:L:Count$CardCounters.LOYALTY
DeckHas:Ability$LifeGain & Ability$Token
Oracle:[+2]: You gain life equal to the greatest power among creatures you control.\n[0]: Create a 3/3 green Dinosaur creature token with trample.\n[-X]: Huatli, Warrior Poet deals X damage divided as you choose among any number of target creatures. Creatures dealt damage this way can't block this turn.

View File

@@ -3,8 +3,7 @@ ManaCost:4 U U
Types:Creature Human Advisor Mutant
PT:0/0
K:Graft:4
A:AB$ Draw | Cost$ 1 SubCounter<2/P1P1/Creature/among creatures you control> | NumCards$ 1 | SpellDescription$ Draw a card.
A:AB$ Draw | Cost$ 1 RemoveAnyCounter<2/P1P1/Creature.YouCtrl/among creatures you control> | NumCards$ 1 | SpellDescription$ Draw a card.
DeckNeeds:Ability$Counters
DeckHas:Ability$Counters
SVar:Picture:http://www.wizards.com/global/images/magic/general/novijen_sages.jpg
Oracle:Graft 4 (This creature enters the battlefield with four +1/+1 counters on it. Whenever another creature enters the battlefield, you may move a +1/+1 counter from this creature onto it.)\n{1}, Remove two +1/+1 counters from among creatures you control: Draw a card.

View File

@@ -1,9 +1,8 @@
Name:Ooze Flux
ManaCost:3 G
Types:Enchantment
A:AB$ Token | Announce$ X | Cost$ XCantBe0 1 G SubCounter<X/P1P1/Creature/among creatures you control> | TokenAmount$ 1 | TokenScript$ g_x_x_ooze | TokenOwner$ You | LegacyImage$ g x x ooze gtc | TokenPower$ X | TokenToughness$ X | SpellDescription$ Create an X/X green Ooze creature token, where X is the number of +1/+1 counters removed this way.
A:AB$ Token | Cost$ XCantBe0 1 G RemoveAnyCounter<X/P1P1/Creature.YouCtrl/among creatures you control> | TokenAmount$ 1 | TokenScript$ g_x_x_ooze | TokenOwner$ You | LegacyImage$ g x x ooze gtc | TokenPower$ X | TokenToughness$ X | SpellDescription$ Create an X/X green Ooze creature token, where X is the number of +1/+1 counters removed this way.
SVar:X:Count$xPaid
AI:RemoveDeck:All
DeckHints:Ability$Counters
SVar:Picture:http://www.wizards.com/global/images/magic/general/ooze_flux.jpg
Oracle:{1}{G}, Remove one or more +1/+1 counters from among creatures you control: Create an X/X green Ooze creature token, where X is the number of +1/+1 counters removed this way.

View File

@@ -3,11 +3,9 @@ ManaCost:1 G
Types:Creature Spirit
PT:1/2
T:Mode$ SpellCast | ValidCard$ Spirit,Arcane | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter | TriggerDescription$ Whenever you cast a Spirit or Arcane spell, you may put a ki counter on CARDNAME.
SVar:TrigPutCounter:DB$PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
#ChosenX SVar created by Cost payment
A:AB$ Mana | Cost$ 1 SubCounter<X/KI> | Produced$ Any | Amount$ ChosenX | References$ X | AILogic$ ManaRitual | AINoRecursiveCheck$ True | SpellDescription$ Add X mana of any one color.
SVar:X:XChoice
SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
A:AB$ Mana | Cost$ 1 SubCounter<X/KI> | Produced$ Any | Amount$ X | References$ X | AILogic$ ManaRitual | AINoRecursiveCheck$ True | SpellDescription$ Add X mana of any one color.
SVar:X:Count$xPaid
AI:RemoveDeck:Random
DeckHints:Type$Spirit|Arcane
SVar:Picture:http://www.wizards.com/global/images/magic/general/petalmane_baku.jpg
Oracle:Whenever you cast a Spirit or Arcane spell, you may put a ki counter on Petalmane Baku.\n{1}, Remove X ki counters from Petalmane Baku: Add X mana of any one color.

View File

@@ -1,9 +1,8 @@
Name:Power Conduit
ManaCost:2
Types:Artifact
A:AB$ Charm | Cost$ T RemoveAnyCounter<1/Permanent.YouCtrl/a permanent you control> | Choices$ ConduitCharge,ConduitP1P1 | Defined$ You
A:AB$ Charm | Cost$ T RemoveAnyCounter<1/Any/Permanent.YouCtrl/a permanent you control> | Choices$ ConduitCharge,ConduitP1P1 | Defined$ You
SVar:ConduitCharge:DB$ PutCounter | ValidTgts$ Artifact | TgtPrompt$ Select target artifact | CounterType$ CHARGE | CounterNum$ 1 | SpellDescription$ Put a charge counter on target artifact.
SVar:ConduitP1P1:DB$ PutCounter | ValidTgts$ Creature | TgtPrompt$ Select target creature | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on target creature.
SVar:AIRemoveCounterCostPriority:ANY
SVar:Picture:http://www.wizards.com/global/images/magic/general/power_conduit.jpg
Oracle:{T}, Remove a counter from a permanent you control: Choose one —\n• Put a charge counter on target artifact.\n• Put a +1/+1 counter on target creature.

View File

@@ -3,10 +3,10 @@ ManaCost:4 U
Types:Creature Spirit
PT:3/3
T:Mode$ SpellCast | ValidCard$ Spirit,Arcane | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter | TriggerDescription$ Whenever you cast a Spirit or Arcane spell, you may put a ki counter on CARDNAME.
SVar:TrigPutCounter:DB$PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
A:AB$ ChangeZone | Cost$ 1 T SubCounter<X/KI> | Origin$ Battlefield | Destination$ Hand | ValidTgts$ Creature | ChangeNum$ 1 | References$ X | AITgtBeforeCostEval$ True | SpellDescription$ Return target creature with converted mana cost X or less to its owner's hand.
SVar:X:Targeted$CardManaCost
SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
A:AB$ ChangeZone | Cost$ 1 T SubCounter<X/KI> | Origin$ Battlefield | Destination$ Hand | ValidTgts$ Creature.cmcLEX | ChangeNum$ 1 | References$ X | SpellDescription$ Return target creature with converted mana cost X or less to its owner's hand.
SVar:X:Count$xPaid
AI:RemoveDeck:Random
DeckHints:Type$Spirit|Arcane
# We'll need to improve the script at some stage, especially if we add Hunter of Eyeblights or Razorfin Abolisher.
SVar:Picture:http://www.wizards.com/global/images/magic/general/quillmane_baku.jpg
Oracle:Whenever you cast a Spirit or Arcane spell, you may put a ki counter on Quillmane Baku.\n{1}, {T}, Remove X ki counters from Quillmane Baku: Return target creature with converted mana cost X or less to its owner's hand.

View File

@@ -1,9 +1,8 @@
Name:Retribution of the Ancients
ManaCost:B
Types:Enchantment
A:AB$ Pump | Announce$ X | Cost$ B SubCounter<X/P1P1/Creature/among creatures you control> | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ -X | NumDef$ -X | References$ X | IsCurse$ True | SpellDescription$ Target creature gets -X/-X until end of turn.
A:AB$ Pump | Cost$ B RemoveAnyCounter<X/P1P1/Creature.YouCtrl/among creatures you control> | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ -X | NumDef$ -X | References$ X | IsCurse$ True | SpellDescription$ Target creature gets -X/-X until end of turn.
SVar:X:Count$xPaid
AI:RemoveDeck:All
DeckNeeds:Ability$Counters
SVar:Picture:http://www.wizards.com/global/images/magic/general/retribution_of_the_ancients.jpg
Oracle:{B}, Remove X +1/+1 counters from among creatures you control: Target creature gets -X/-X until end of turn.

View File

@@ -3,11 +3,9 @@ ManaCost:3 B B
Types:Creature Spirit
PT:2/1
T:Mode$ SpellCast | ValidCard$ Spirit,Arcane | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter | TriggerDescription$ Whenever you cast a Spirit or Arcane spell, you may put a ki counter on CARDNAME.
SVar:TrigPutCounter:DB$PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
#ChosenX SVar created by Cost payment
A:AB$ Pump | Cost$ 1 T SubCounter<X/KI> | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ -ChosenX | NumDef$ -ChosenX | IsCurse$ True | AILogic$ DebuffForXCounters | References$ X | SpellDescription$ Target creature gets -X/-X until end of turn.
SVar:X:XChoice
SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
A:AB$ Pump | Cost$ 1 T SubCounter<X/KI> | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ -X | NumDef$ -X | IsCurse$ True | References$ X | SpellDescription$ Target creature gets -X/-X until end of turn.
SVar:X:Count$xPaid
AI:RemoveDeck:Random
DeckHints:Type$Spirit|Arcane
SVar:Picture:http://www.wizards.com/global/images/magic/general/skullmane_baku.jpg
Oracle:Whenever you cast a Spirit or Arcane spell, you may put a ki counter on Skullmane Baku.\n{1}, {T}, Remove X ki counters from Skullmane Baku: Target creature gets -X/-X until end of turn.

View File

@@ -2,6 +2,6 @@ Name:Soul Diviner
ManaCost:U B
Types:Creature Zombie Wizard
PT:2/3
A:AB$ Draw | Cost$ T RemoveAnyCounter<1/Card.Artifact;Card.Creature;Card.Land;Card.Planeswalker/artifact, creature, land or planeswalker> | NumCards$ 1 | SpellDescription$ Draw a card.
A:AB$ Draw | Cost$ T RemoveAnyCounter<1/Any/Card.Artifact;Card.Creature;Card.Land;Card.Planeswalker/artifact, creature, land or planeswalker> | NumCards$ 1 | SpellDescription$ Draw a card.
AI:RemoveDeck:Random
Oracle:{T}, Remove a counter from an artifact, creature, land, or planeswalker you control: Draw a card.

View File

@@ -0,0 +1,12 @@
Name:Tayam, Luminous Enigma
ManaCost:1 W B G
Types:Legendary Creature Nightmare Beast
PT:3/3
K:ETBReplacement:Other:AddExtraCounter:Mandatory:Battlefield:Creature.Other+YouCtrl
SVar:AddExtraCounter:DB$ PutCounter | ETB$ True | Defined$ ReplacedCard | CounterType$ Vigilance | CounterNum$ 1 | SpellDescription$ Each other creature you control enters the battlefield with an additional vigilance counter on it.
A:AB$ Mill | Cost$ 3 RemoveAnyCounter<3/Any/Permanent.YouCtrl/among permanents you control> | NumCards$ 3 | Defined$ You | SubAbility$ DBChangeZone | StackDescription$ SpellDescription | SpellDescription$ Mill three cards, then return a permanent card with converted mana cost 3 or less from your graveyard to the battlefield.
SVar:DBChangeZone:DB$ ChangeZone | Hidden$ True | Mandatory$ True | ChangeType$ Permanent.YouOwn+cmcLE3 | ChangeNum$ 1 | Origin$ Graveyard | Destination$ Battlefield
DeckHas:Ability$Counters
SVar:AIRemoveCounterCostPriority:Vigilance
Oracle:Each other creature you control enters the battlefield with an additional vigilance counter on it.\n{3}, Remove three counters from among creatures you control: Mill three cards, then return a permanent card with converted mana cost 3 or less from your graveyard to the battlefield.

View File

@@ -3,10 +3,9 @@ ManaCost:2 W
Types:Creature Spirit
PT:2/2
T:Mode$ SpellCast | ValidCard$ Spirit,Arcane | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ TrigPutCounter | TriggerDescription$ Whenever you cast a Spirit or Arcane spell, you may put a ki counter on CARDNAME.
SVar:TrigPutCounter:DB$PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
A:AB$ Tap | Announce$ X | XMaxLimit$ Ki | Cost$ 1 SubCounter<X/KI> | CostDesc$ {1}, Remove X ki counters from CARDNAME: | TargetMin$ X | TargetMax$ X | ValidTgts$ Creature | IsCurse$ True | AILogic$ TapForXCounters | TgtPrompt$ Select X target creatures | References$ X,Ki | SpellDescription$ Tap X target creatures.
SVar:X:XChoice
SVar:Ki:Count$CardCounters.KI
SVar:TrigPutCounter:DB$ PutCounter | Defined$ Self | CounterType$ KI | CounterNum$ 1
A:AB$ Tap | Cost$ 1 SubCounter<X/KI> | TargetMin$ X | TargetMax$ X | ValidTgts$ Creature | IsCurse$ True | TgtPrompt$ Select X target creatures | References$ X | SpellDescription$ Tap X target creatures.
SVar:X:Count$xPaid
AI:RemoveDeck:Random
DeckHints:Type$Spirit|Arcane
Oracle:Whenever you cast a Spirit or Arcane spell, you may put a ki counter on Waxmane Baku.\n{1}, Remove X ki counters from Waxmane Baku: Tap X target creatures.

View File

@@ -3,18 +3,18 @@ package forge.player;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.CardType;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameEntityCounterTable;
import forge.game.GameEntityView;
import forge.game.GameEntityViewMap;
import forge.game.ability.AbilityUtils;
@@ -907,57 +907,59 @@ public class HumanCostDecision extends CostDecisionMakerBase {
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
CardCollectionView list = new CardCollection(player.getCardsIn(ZoneType.Battlefield));
list = CardLists.getValidCards(list, type.split(";"), player, source, ability);
CardCollectionView list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"), player, source, ability);
list = CardLists.filter(list, CardPredicates.hasCounters());
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
return card.hasCounters();
}
});
final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, list, ability);
inp.setMessage(Localizer.getInstance().getMessage("lblSelectTargetCounter", cost.getDescriptiveType()));
inp.setCancelAllowed(false);
final InputSelectCardToRemoveCounter inp = new InputSelectCardToRemoveCounter(controller, c, cost, cost.counter, list, ability);
inp.setCancelAllowed(true);
inp.showAndWait();
final Card selected = inp.getFirstSelected();
final Map<CounterType, Integer> tgtCounters = selected.getCounters();
final List<CounterType> typeChoices = new ArrayList<>();
for (final CounterType key : tgtCounters.keySet()) {
if (tgtCounters.get(key) > 0) {
typeChoices.add(key);
}
if (inp.hasCancelled()) {
return null;
}
final String prompt = Localizer.getInstance().getMessage("lblSelectRemoveCounterType");
cost.setCounterType(controller.getGui().one(prompt, typeChoices));
return PaymentDecision.card(selected, cost.getCounter());
return PaymentDecision.counters(inp.getCounterTable());
}
public static final class InputSelectCardToRemoveCounter extends InputSelectManyBase<Card> {
public static final class InputSelectCardToRemoveCounter extends InputSelectManyBase<GameEntity> {
private static final long serialVersionUID = 2685832214519141903L;
private final Map<Card,Integer> cardsChosen;
private final CounterType counterType;
private final CardCollectionView validChoices;
public InputSelectCardToRemoveCounter(final PlayerControllerHuman controller, final int cntCounters, final CounterType cType, final CardCollectionView validCards, final SpellAbility sa) {
private final GameEntityCounterTable counterTable = new GameEntityCounterTable();
public InputSelectCardToRemoveCounter(final PlayerControllerHuman controller, final int cntCounters, final CostPart costPart, final CounterType cType, final CardCollectionView validCards, final SpellAbility sa) {
super(controller, cntCounters, cntCounters, sa);
this.validChoices = validCards;
counterType = cType;
cardsChosen = cntCounters > 0 ? new HashMap<>() : null;
setMessage(Localizer.getInstance().getMessage("lblRemoveNTargetCounterFromCardPayCostConfirm", "%d", counterType == null ? "any" : counterType.getName(), costPart.getDescriptiveType()));
}
@Override
protected boolean onCardSelected(final Card c, final List<Card> otherCardsToSelect, final ITriggerEvent triggerEvent) {
if (!isValidChoice(c) || c.getCounters(counterType) <= getTimesSelected(c)) {
if (!isValidChoice(c)) {
return false;
}
final int tc = getTimesSelected(c);
cardsChosen.put(c, tc + 1);
CounterType cType = this.counterType;
if (cType == null) {
Map<CounterType, Integer> cmap = counterTable.filterToRemove(c);
String prompt = Localizer.getInstance().getMessage("lblSelectCountersTypeToRemove");
cType = getController().chooseCounterType(Lists.newArrayList(cmap.keySet()), sa, prompt, null);
}
if (cType == null) {
return false;
}
if (c.getCounters(cType) <= counterTable.get(c, cType)) {
return false;
}
counterTable.put(c, cType, 1);
onSelectStateChanged(c, true);
refresh();
@@ -966,9 +968,25 @@ public class HumanCostDecision extends CostDecisionMakerBase {
@Override
public String getActivateAction(final Card c) {
if (!isValidChoice(c) || c.getCounters(counterType) <= getTimesSelected(c)) {
if (!isValidChoice(c)) {
return null;
}
if (counterType != null) {
if (c.getCounters(counterType) <= counterTable.get(c, counterType)) {
return null;
}
} else {
boolean found = false;
for (Map.Entry<CounterType, Integer> e : c.getCounters().entrySet()) {
if (e.getValue() > counterTable.get(c, e.getKey())) {
found = true;
break;
}
}
if (!found) {
return null;
}
}
return Localizer.getInstance().getMessage("lblRemoveCounterFromCard");
}
@@ -992,9 +1010,11 @@ public class HumanCostDecision extends CostDecisionMakerBase {
private int getDistibutedCounters() {
int sum = 0;
for (final Entry<Card, Integer> kv : cardsChosen.entrySet()) {
sum += kv.getValue().intValue();
for (Integer v : this.counterTable.values()) {
sum += v;
}
return sum;
}
@@ -1002,13 +1022,13 @@ public class HumanCostDecision extends CostDecisionMakerBase {
return validChoices.contains(choice);
}
public int getTimesSelected(final Card c) {
return cardsChosen.containsKey(c) ? cardsChosen.get(c).intValue() : 0;
public GameEntityCounterTable getCounterTable() {
return this.counterTable;
}
@Override
public Collection<Card> getSelected() {
return cardsChosen.keySet();
public Collection<GameEntity> getSelected() {
return counterTable.rowKeySet();
}
}
@@ -1063,45 +1083,28 @@ public class HumanCostDecision extends CostDecisionMakerBase {
return PaymentDecision.card(ability.getOriginalHost(), cntRemoved >= 0 ? cntRemoved : maxCounters);
}
final CardCollectionView validCards = CardLists.getValidCards(player.getCardsIn(cost.zone), type.split(";"), player, source, ability);
if (cost.zone.equals(ZoneType.Battlefield)) {
if (cntRemoved == 0) {
return PaymentDecision.card(source, 0);
}
final InputSelectCardToRemoveCounter inp = new InputSelectCardToRemoveCounter(controller, cntRemoved, cost.counter, validCards, ability);
inp.setMessage(Localizer.getInstance().getMessage("lblRemoveNTargetCounterFromCardPayCostConfirm", "%d", cost.counter.getName(), cost.getDescriptiveType()));
inp.setCancelAllowed(true);
inp.showAndWait();
if (inp.hasCancelled()) {
return null;
}
// Have to hack here: remove all counters minus one, without firing any triggers,
// triggers will fire when last is removed by executePayment.
// They don't care how many were removed anyway
// int sum = 0;
for (final Card crd : inp.getSelected()) {
final int removed = inp.getTimesSelected(crd);
// sum += removed;
if (removed < 2) {
continue;
}
final int oldVal = crd.getCounters().get(cost.counter).intValue();
crd.getCounters().put(cost.counter, Integer.valueOf(oldVal - removed + 1));
}
return PaymentDecision.card(inp.getSelected(), 1);
}
// Rift Elemental only - always removes 1 counter, so there will be no code for N counters.
GameEntityViewMap<Card, CardView> gameCacheSuspended = GameEntityView.getMap(CardLists.filter(validCards, CardPredicates.hasCounter(cost.counter)));
final CardView cv = controller.getGui().oneOrNone(Localizer.getInstance().getMessage("lblRemoveCountersFromAInZoneCard", cost.zone.getTranslatedName()), gameCacheSuspended.getTrackableKeys());
if (cv == null || !gameCacheSuspended.containsKey(cv)) {
CardCollectionView validCards = CardLists.getValidCards(player.getCardsIn(cost.zone), type.split(";"), player, source, ability);
// you can only select 1 card to remove N counters from
validCards = CardLists.filter(validCards, CardPredicates.hasCounter(cost.counter, cntRemoved));
if (validCards.isEmpty()) {
return null;
}
return PaymentDecision.card(gameCacheSuspended.get(cv), c);
final InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, validCards, ability);
inp.setMessage(Localizer.getInstance().getMessage("lblRemoveCountersFromAInZoneCard", cost.zone.getTranslatedName()));
inp.setCancelAllowed(true);
inp.showAndWait();
if (inp.hasCancelled()) {
return null;
}
final Card selected = inp.getFirstSelected();
if (selected == null) {
return null;
}
return PaymentDecision.card(selected, cntRemoved);
}
@Override

View File

@@ -1,6 +1,6 @@
package forge.player;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.FThreads;
import forge.card.mana.ManaCost;
@@ -29,12 +29,10 @@ import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import forge.util.gui.SGuiChoose;
import forge.util.Localizer;
import forge.util.CardTranslation;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class HumanPlay {
@@ -362,74 +360,20 @@ public class HumanPlay {
part.payAsDecided(p, pd, sourceAbility);
}
else if (part instanceof CostRemoveCounter) {
CounterType counterType = ((CostRemoveCounter) part).counter;
int amount = getAmountFromPartX(part, source, sourceAbility);
PaymentDecision pd = part.accept(hcd);
if (!part.canPay(sourceAbility, p)) {
if (pd == null)
return false;
}
if (!mandatory) {
if (!p.getController().confirmPayment(part, Localizer.getInstance().getMessage("lblDoYouWantRemoveNTargetTypeCounterFromCard", String.valueOf(amount), counterType.getName(), CardTranslation.getTranslatedName(source.getName())), sourceAbility)) {
return false;
}
}
source.subtractCounter(counterType, amount);
else
part.payAsDecided(p, pd, sourceAbility);
}
else if (part instanceof CostRemoveAnyCounter) {
int amount = getAmountFromPartX(part, source, sourceAbility);
CardCollectionView list = p.getCardsIn(ZoneType.Battlefield);
int allCounters = 0;
for (Card c : list) {
final Map<CounterType, Integer> tgtCounters = c.getCounters();
for (Integer value : tgtCounters.values()) {
allCounters += value;
}
}
if (allCounters < amount) { return false; }
PaymentDecision pd = part.accept(hcd);
if (!mandatory) {
if (!p.getController().confirmPayment(part, Localizer.getInstance().getMessage("lblDoYouWantRemoveCountersFromCard", part.getDescriptiveType()), sourceAbility)) {
return false;
}
}
list = CardLists.getValidCards(list, part.getType().split(";"), p, source, sourceAbility);
while (amount > 0) {
final CounterType counterType;
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card card) {
return card.hasCounters();
}
});
if (list.isEmpty()) { return false; }
InputSelectCardsFromList inp = new InputSelectCardsFromList(controller, 1, 1, list, sourceAbility);
inp.setMessage(Localizer.getInstance().getMessage("lblSelectRemoveCounterCard"));
inp.setCancelAllowed(true);
inp.showAndWait();
if (inp.hasCancelled()) {
continue;
}
Card selected = inp.getFirstSelected();
final Map<CounterType, Integer> tgtCounters = selected.getCounters();
final List<CounterType> typeChoices = new ArrayList<>();
for (CounterType key : tgtCounters.keySet()) {
if (tgtCounters.get(key) > 0) {
typeChoices.add(key);
}
}
if (typeChoices.size() > 1) {
String cprompt = Localizer.getInstance().getMessage("lblSelectRemoveCounterType");
counterType = controller.getGui().one(cprompt, typeChoices);
}
else {
counterType = typeChoices.get(0);
}
selected.subtractCounter(counterType, 1);
amount--;
}
if (pd == null)
return false;
else
part.payAsDecided(p, pd, sourceAbility);
}
else if (part instanceof CostExile) {
CostExile costExile = (CostExile) part;

View File

@@ -29,7 +29,6 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardPlayOption;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostPayment;
import forge.game.keyword.KeywordInterface;
@@ -256,16 +255,7 @@ public class HumanPlaySpellAbility {
}
if (needX && manaCost != null) {
boolean xInCost = manaCost.getAmountOfX() > 0;
if (!xInCost) {
for (final CostPart part : cost.getCostParts()) {
if (part.getAmount().equals("X")) {
xInCost = true;
break;
}
}
}
if (xInCost) {
if (cost.hasXInAnyCostPart()) {
final String sVar = ability.getSVar("X"); //only prompt for new X value if card doesn't determine it another way
if ("Count$xPaid".equals(sVar) || sVar.isEmpty()) {
final Integer value = controller.announceRequirements(ability, "X", allowZero && manaCost.canXbe0());

View File

@@ -321,8 +321,27 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
public Integer announceRequirements(final SpellAbility ability, final String announce,
final boolean canChooseZero) {
final int min = canChooseZero ? 0 : 1;
final int max = ability.hasParam("XMaxLimit") ? AbilityUtils.calculateAmount(ability.getHostCard(),
ability.getParam("XMaxLimit"), ability) : Integer.MAX_VALUE;
int max = Integer.MAX_VALUE;
if ("X".equals(announce)) {
Cost cost = ability.getPayCosts();
if (ability.hasParam("XMaxLimit")) {
max = Math.min(max, AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("XMaxLimit"), ability));
}
if (cost != null) {
Integer costX = cost.getMaxForNonManaX(ability, player);
if (costX != null) {
max = Math.min(max, min);
}
}
}
if (ability.usesTargeting()) {
// if announce is used as min targets, check what the max possible number would be
if (announce.equals(ability.getTargetRestrictions().getMinTargets())) {
max = Math.min(max, CardUtil.getValidCardsToTarget(ability.getTargetRestrictions(), ability).size());
}
}
return getGui().getInteger(localizer.getMessage("lblChooseAnnounceForCard", announce,
CardTranslation.getTranslatedName(ability.getHostCard().getName())) , min, max, min + 9);
}

View File

@@ -71,11 +71,10 @@ public class TargetSelection {
}
public final boolean chooseTargets(Integer numTargets) {
final TargetRestrictions tgt = getTgt();
final boolean canTarget = tgt != null && tgt.doesTarget();
if (!canTarget) {
if (!ability.usesTargeting()) {
throw new RuntimeException("TargetSelection.chooseTargets called for ability that does not target - " + ability);
}
final TargetRestrictions tgt = getTgt();
// Number of targets is explicitly set only if spell is being redirected (ex. Swerve or Redirect)
final int minTargets = numTargets != null ? numTargets.intValue() : tgt.getMinTargets(ability.getHostCard(), ability);