Merge remote-tracking branch 'upstream/master' into collector-number-in-card-list-and-card-db-refactoring

This commit is contained in:
leriomaggio
2021-07-06 08:36:40 +01:00
1192 changed files with 9811 additions and 4628 deletions

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@
.vscode/settings.json
.vscode/launch.json
.factorypath
# Ignore NetBeans config files

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.40-SNAPSHOT</version>
<version>1.6.43-SNAPSHOT</version>
</parent>
<artifactId>forge-ai</artifactId>

View File

@@ -18,6 +18,7 @@
package forge.ai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.google.common.base.Predicate;
@@ -43,6 +44,7 @@ import forge.game.combat.GlobalAttackRestrictions;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -391,7 +393,7 @@ public class AiAttackController {
//Calculate the amount of creatures necessary
for (int i = 0; i < list.size(); i++) {
if (!this.doesHumanAttackAndWin(ai, i)) {
if (!doesHumanAttackAndWin(ai, i)) {
blockersNeeded = i;
break;
}
@@ -635,7 +637,7 @@ public class AiAttackController {
if (defs.size() == 1) {
return defs.getFirst();
}
Player prefDefender = (Player) (defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0));
GameEntity prefDefender = defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0);
// Attempt to see if there's a defined entity that must be attacked strictly this turn...
GameEntity entity = ai.getMustAttackEntityThisTurn();
@@ -659,7 +661,8 @@ public class AiAttackController {
// 2. attack planeswalkers
List<Card> pwDefending = c.getDefendingPlaneswalkers();
if (!pwDefending.isEmpty()) {
return pwDefending.get(0);
final Card pwNearUlti = ComputerUtilCard.getBestPlaneswalkerToDamage(pwDefending);
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
} else {
return prefDefender;
}
@@ -695,14 +698,14 @@ public class AiAttackController {
tradeIfLowerLifePressure = aic.getBooleanProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
}
final boolean bAssault = this.doAssault(ai);
final boolean bAssault = doAssault(ai);
// TODO: detect Lightmine Field by presence of a card with a specific trigger
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field");
// TODO: detect Season of the Witch by presence of a card with a specific trigger
final boolean seasonOfTheWitch = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Season of the Witch");
// Determine who will be attacked
GameEntity defender = this.chooseDefender(combat, bAssault);
GameEntity defender = chooseDefender(combat, bAssault);
List<Card> attackersLeft = new ArrayList<>(this.attackers);
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
@@ -1090,17 +1093,18 @@ public class AiAttackController {
// if enough damage: switch to next planeswalker or player
if (damage >= pw.getCounters(CounterEnumType.LOYALTY)) {
List<Card> pwDefending = combat.getDefendingPlaneswalkers();
boolean found = false;
// look for next planeswalker
for (Card walker : pwDefending) {
if (combat.getAttackersOf(walker).isEmpty()) {
defender = walker;
found = true;
break;
for (Card walker : Lists.newArrayList(pwDefending)) {
if (!combat.getAttackersOf(walker).isEmpty()) {
pwDefending.remove(walker);
}
}
if (!found) {
defender = combat.getDefendingPlayers().get(0);
if (pwDefending.isEmpty()) {
defender = Collections.min(Lists.newArrayList(combat.getDefendingPlayers()), PlayerPredicates.compareByLife());
}
else {
final Card pwNearUlti = ComputerUtilCard.getBestPlaneswalkerToDamage(pwDefending);
defender = pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
}
}
}
@@ -1354,8 +1358,7 @@ public class AiAttackController {
}
// if card has a Exert Trigger which would target,
// but there are no creatures it can target, no need to exert with
// it
// but there are no creatures it can target, no need to exert with it
boolean missTarget = false;
for (Trigger t : c.getTriggers()) {
if (!TriggerType.Exerted.equals(t.getMode())) {

View File

@@ -354,7 +354,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.number(c);
}
@Override
public PaymentDecision visit(CostPutCardToLib cost) {
if (cost.payCostFromSource()) {

View File

@@ -331,11 +331,14 @@ public class ComputerUtil {
}
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
return getCardPreference(ai, activate, pref, typeList, null);
}
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList, SpellAbility sa) {
final Game game = ai.getGame();
String prefDef = "";
if (activate != null) {
prefDef = activate.getSVar("AIPreference");
final String[] prefGroups = activate.getSVar("AIPreference").split("\\|");
final String[] prefGroups = prefDef.split("\\|");
for (String prefGroup : prefGroups) {
final String[] prefValid = prefGroup.trim().split("\\$");
if (prefValid[0].equals(pref) && !prefValid[1].startsWith("Special:")) {
@@ -346,8 +349,8 @@ public class ComputerUtil {
for (String validItem : prefValid[1].split(",")) {
final CardCollection prefList = CardLists.getValidCards(typeList, validItem, activate.getController(), activate, null);
int threshold = getAIPreferenceParameter(activate, "CreatureEvalThreshold");
int minNeeded = getAIPreferenceParameter(activate, "MinCreaturesBelowThreshold");
int threshold = getAIPreferenceParameter(activate, "CreatureEvalThreshold", sa);
int minNeeded = getAIPreferenceParameter(activate, "MinCreaturesBelowThreshold", sa);
if (threshold != -1) {
List<Card> toRemove = Lists.newArrayList();
@@ -390,7 +393,7 @@ public class ComputerUtil {
final CardCollection sacMeList = CardLists.filter(typeList, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return (c.hasSVar("SacMe") && (Integer.parseInt(c.getSVar("SacMe")) == priority));
return c.hasSVar("SacMe") && (Integer.parseInt(c.getSVar("SacMe")) == priority);
}
});
if (!sacMeList.isEmpty()) {
@@ -419,6 +422,7 @@ public class ComputerUtil {
if (!nonCreatures.isEmpty()) {
return ComputerUtilCard.getWorstAI(nonCreatures);
} else if (!typeList.isEmpty()) {
// TODO make sure survival is possible in case the creature blocks a trampler
return ComputerUtilCard.getWorstAI(typeList);
}
}
@@ -505,7 +509,7 @@ public class ComputerUtil {
return null;
}
public static int getAIPreferenceParameter(final Card c, final String paramName) {
public static int getAIPreferenceParameter(final Card c, final String paramName, SpellAbility sa) {
if (!c.hasSVar("AIPreferenceParams")) {
return -1;
}
@@ -520,7 +524,21 @@ public class ComputerUtil {
case "CreatureEvalThreshold":
// Threshold of 150 is just below the level of a 1/1 mana dork or a 2/2 baseline creature with no keywords
if (paramName.equals(parName)) {
return Integer.parseInt(parValue);
int num = 0;
try {
num = Integer.parseInt(parValue);
} catch (NumberFormatException nfe) {
String[] valParts = StringUtils.split(parValue, "/");
CardCollection foundCards = AbilityUtils.getDefinedCards(c, valParts[0], sa);
if (!foundCards.isEmpty()) {
num = ComputerUtilCard.evaluateCreature(foundCards.get(0));
}
valParts[0] = Integer.toString(num);
if (valParts.length > 1) {
num = AbilityUtils.doXMath(num, valParts[1], c, sa);
}
}
return num;
}
break;
case "MinCreaturesBelowThreshold":
@@ -543,9 +561,8 @@ public class ComputerUtil {
typeList = CardLists.filter(typeList, CardPredicates.canBeSacrificedBy(ability));
if ((target != null) && target.getController() == ai) {
typeList.remove(target); // don't sacrifice the card we're pumping
}
// don't sacrifice the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, ability, ai);
if (typeList.size() < amount) {
return null;
@@ -573,9 +590,8 @@ public class ComputerUtil {
final Card target, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"), activate.getController(), activate, sa);
if ((target != null) && target.getController() == ai) {
typeList.remove(target); // don't exile the card we're pumping
}
// don't exile the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai);
if (typeList.size() < amount) {
return null;
@@ -594,9 +610,8 @@ public class ComputerUtil {
final Card target, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(zone), type.split(";"), activate.getController(), activate, sa);
if ((target != null) && target.getController() == ai) {
typeList.remove(target); // don't move the card we're pumping
}
// don't move the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai);
if (typeList.size() < amount) {
return null;
@@ -718,12 +733,10 @@ public class ComputerUtil {
}
public static CardCollection chooseReturnType(final Player ai, final String type, final Card activate, final Card target, final int amount, SpellAbility sa) {
final CardCollection typeList =
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
if ((target != null) && target.getController() == ai) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
// don't bounce the card we're pumping
typeList.remove(target);
}
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai);
if (typeList.size() < amount) {
return new CardCollection();
@@ -743,7 +756,7 @@ public class ComputerUtil {
CardCollection remaining = new CardCollection(cardlist);
final CardCollection sacrificed = new CardCollection();
final Card host = source.getHostCard();
final int considerSacThreshold = getAIPreferenceParameter(host, "CreatureEvalThreshold");
final int considerSacThreshold = getAIPreferenceParameter(host, "CreatureEvalThreshold", source);
if ("OpponentOnly".equals(source.getParam("AILogic"))) {
if(!source.getActivatingPlayer().isOpponentOf(ai)) {
@@ -3026,6 +3039,6 @@ public class ComputerUtil {
}
}
return false;
}
}
}

View File

@@ -47,6 +47,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostPayEnergy;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordCollection;
import forge.game.keyword.KeywordInterface;
@@ -137,6 +138,56 @@ public class ComputerUtilCard {
return Aggregates.itemWithMin(all, CardPredicates.Accessors.fnGetCmc);
}
public static Card getBestPlaneswalkerToDamage(final List<Card> pws) {
Card bestTgt = null;
// As of right now, ranks planeswalkers by their Current Loyalty * 10 + Big buff if close to "Ultimate"
int bestScore = 0;
for (Card pw : pws) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
int pwScore = curLoyalty * 10;
for (SpellAbility sa : pw.getSpellAbilities()) {
if (sa.hasParam("Ultimate")) {
Integer loyaltyCost = 0;
CostRemoveCounter remLoyalty = sa.getPayCosts().getCostPartByType(CostRemoveCounter.class);
if (remLoyalty != null) {
// if remLoyalty is null, generally there's an AddCounter<0/LOYALTY> cost, like for Gideon Jura.
loyaltyCost = remLoyalty.convertAmount();
}
if (loyaltyCost != null && loyaltyCost != 0 && loyaltyCost - curLoyalty <= 1) {
// Will ultimate soon
pwScore += 10000;
}
if (pwScore > bestScore) {
bestScore = pwScore;
bestTgt = pw;
}
}
}
}
return bestTgt;
}
public static Card getWorstPlaneswalkerToDamage(final List<Card> pws) {
Card bestTgt = null;
int bestScore = Integer.MAX_VALUE;
for (Card pw : pws) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
if (curLoyalty < bestScore) {
bestScore = curLoyalty;
bestTgt = pw;
}
}
return bestTgt;
}
// The AI doesn't really pick the best enchantment, just the most expensive.
/**
* <p>
@@ -217,6 +268,63 @@ public class ComputerUtilCard {
return Aggregates.random(bLand); // random tapped land of least represented type
}
/**
* <p>
* getWorstLand.
* </p>
*
* @param lands
* @return a {@link forge.game.card.Card} object.
*/
public static Card getWorstLand(final List<Card> lands) {
Card worstLand = null;
int maxScore = Integer.MIN_VALUE;
// first, check for tapped, basic lands
for (Card tmp : lands) {
int score = tmp.isTapped() ? 2 : 0;
score += tmp.isBasicLand() ? 1 : 0;
score -= tmp.isCreature() ? 4 : 0;
for (Card aura : tmp.getEnchantedBy()) {
if (aura.getController().isOpponentOf(tmp.getController())) {
score += 5;
} else {
score -= 5;
}
}
if (score == maxScore &&
CardLists.count(lands, CardPredicates.sharesNameWith(tmp)) > CardLists.count(lands, CardPredicates.sharesNameWith(worstLand))) {
worstLand = tmp;
}
if (score > maxScore) {
worstLand = tmp;
maxScore = score;
}
}
return worstLand;
}
public static Card getBestLandToAnimate(final Iterable<Card> lands) {
Card land = null;
int maxScore = Integer.MIN_VALUE;
// first, check for tapped, basic lands
for (Card tmp : lands) {
int score = tmp.isTapped() ? 0 : 2;
score += tmp.isBasicLand() ? 2 : 0;
score -= tmp.isCreature() ? 4 : 0;
score -= 5 * tmp.getEnchantedBy().size();
if (score == maxScore &&
CardLists.count(lands, CardPredicates.sharesNameWith(tmp)) > CardLists.count(lands, CardPredicates.sharesNameWith(land))) {
land = tmp;
}
if (score > maxScore) {
land = tmp;
maxScore = score;
}
}
return land;
}
/**
* <p>
* getCheapestPermanentAI.
@@ -825,57 +933,6 @@ public class ComputerUtilCard {
return result;
}
/**
* <p>
* getWorstLand.
* </p>
*
* @param lands
* @return a {@link forge.game.card.Card} object.
*/
public static Card getWorstLand(final List<Card> lands) {
Card worstLand = null;
int maxScore = Integer.MIN_VALUE;
// first, check for tapped, basic lands
for (Card tmp : lands) {
int score = tmp.isTapped() ? 2 : 0;
score += tmp.isBasicLand() ? 1 : 0;
score -= tmp.isCreature() ? 4 : 0;
for (Card aura : tmp.getEnchantedBy()) {
if (aura.getController().isOpponentOf(tmp.getController())) {
score += 5;
} else {
score -= 5;
}
}
if (score >= maxScore) {
worstLand = tmp;
maxScore = score;
}
}
return worstLand;
} // end getWorstLand
public static Card getBestLandToAnimate(final Iterable<Card> lands) {
Card land = null;
int maxScore = Integer.MIN_VALUE;
// first, check for tapped, basic lands
for (Card tmp : lands) {
// TODO Improve this by choosing basic lands that I have plenty of mana in
int score = tmp.isTapped() ? 0 : 2;
score += tmp.isBasicLand() ? 2 : 0;
score -= tmp.isCreature() ? 4 : 0;
score -= 5 * tmp.getEnchantedBy().size();
if (score >= maxScore) {
land = tmp;
maxScore = score;
}
}
return land;
} // end getBestLandToAnimate
public static final Predicate<Deck> AI_KNOWS_HOW_TO_PLAY_ALL_CARDS = new Predicate<Deck>() {
@Override
public boolean apply(Deck d) {
@@ -888,6 +945,7 @@ public class ComputerUtilCard {
return true;
}
};
public static List<String> chooseColor(SpellAbility sa, int min, int max, List<String> colorChoices) {
List<String> chosen = new ArrayList<>();
Player ai = sa.getActivatingPlayer();
@@ -1592,7 +1650,7 @@ public class ComputerUtilCard {
*/
public static Card getPumpedCreature(final Player ai, final SpellAbility sa,
final Card c, int toughness, int power, final List<String> keywords) {
Card pumped = CardFactory.copyCard(c, true);
Card pumped = CardFactory.copyCard(c, false);
pumped.setSickness(c.hasSickness());
final long timestamp = c.getGame().getNextTimestamp();
final List<String> kws = new ArrayList<>();
@@ -1630,7 +1688,7 @@ public class ComputerUtilCard {
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
Set<CounterType> types = c.getCounters().keySet();
for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true, null);
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, sa, true, null);
}
//Copies tap-state and extra keywords (auras, equipment, etc.)
if (c.isTapped()) {

View File

@@ -472,7 +472,6 @@ public class ComputerUtilCombat {
* @return a boolean.
*/
public static boolean wouldLoseLife(final Player ai, final Combat combat) {
return (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) < ai.getLife());
}

View File

@@ -8,6 +8,8 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
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 com.google.common.collect.Sets;
@@ -20,6 +22,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterEnumType;
@@ -270,7 +273,10 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
// don't sacrifice the card we're pumping
typeList = paymentChoicesWithoutTargets(typeList, sourceAbility, ai);
int count = 0;
while (count < amount) {
@@ -320,11 +326,14 @@ public class ComputerUtilCost {
}
final CardCollection sacList = new CardCollection();
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, sourceAbility);
// don't sacrifice the card we're pumping
typeList = paymentChoicesWithoutTargets(typeList, sourceAbility, ai);
int count = 0;
while (count < amount) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList, sourceAbility);
if (prefCard == null) {
return false;
}
@@ -337,6 +346,19 @@ public class ComputerUtilCost {
return true;
}
/**
* Check sacrifice cost.
*
* @param cost
* the cost
* @param source
* the source
* @return true, if successful
*/
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
return checkSacrificeCost(ai, cost, source, sourceAbility, true);
}
public static boolean isSacrificeSelfCost(final Cost cost) {
if (cost == null) {
return false;
@@ -397,19 +419,6 @@ public class ComputerUtilCost {
return true;
}
/**
* Check sacrifice cost.
*
* @param cost
* the cost
* @param source
* the source
* @return true, if successful
*/
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
return checkSacrificeCost(ai, cost, source, sourceAbility,true);
}
/**
* <p>
* shouldPayCost.
@@ -420,8 +429,8 @@ public class ComputerUtilCost {
* @param cost
* @return a boolean.
*/
@Deprecated
public static boolean shouldPayCost(final Player ai, final Card hostCard, final Cost cost) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
if (!ai.cantLoseForZeroOrLessLife()) {
@@ -741,4 +750,12 @@ public class ComputerUtilCost {
}
return ObjectUtils.defaultIfNull(val, 0);
}
public static CardCollection paymentChoicesWithoutTargets(Iterable<Card> choices, SpellAbility source, Player ai) {
if (source.usesTargeting()) {
final CardCollection targets = new CardCollection(source.getTargets().getTargetCards());
choices = Iterables.filter(choices, Predicates.not(Predicates.and(CardPredicates.isController(ai), Predicates.in(targets))));
}
return new CardCollection(choices);
}
}

View File

@@ -788,7 +788,6 @@ public class ComputerUtilMana {
String manaProduced = ignoreColor || ignoreType ? MagicColor.toShortString(toPay.getColorMask())
: predictManafromSpellAbility(saPayment, ai, toPay);
// System.out.println(manaProduced);
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
@@ -1072,7 +1071,6 @@ public class ComputerUtilMana {
getComboManaChoice(ai, saPayment, sa, cost);
}
else if (saPayment.getApi() == ApiType.ManaReflected) {
//System.out.println("Evaluate reflected mana of: " + saPayment.getHostCard());
Set<String> reflected = CardUtil.getReflectableManaColors(saPayment);
for (byte c : MagicColor.WUBRG) {
@@ -1281,7 +1279,7 @@ public class ComputerUtilMana {
final AbilityManaPart abMana = manaAb.getManaPart();
if (abMana.isComboMana()) {
int amount = manaAb.hasParam("Amount") ? AbilityUtils.calculateAmount(source, manaAb.getParam("Amount"), saRoot) : 1;
int amount = manaAb.hasParam("Amount") ? AbilityUtils.calculateAmount(source, manaAb.getParam("Amount"), manaAb) : 1;
final ManaCostBeingPaid testCost = new ManaCostBeingPaid(cost);
final String[] comboColors = abMana.getComboColors().split(" ");
for (int nMana = 1; nMana <= amount; nMana++) {
@@ -1301,7 +1299,7 @@ public class ComputerUtilMana {
if (!testCost.isPaid()) {
// Loop over combo colors
for (String color : comboColors) {
if (satisfiesColorChoice(abMana, choiceString, choice) && testCost.isAnyPartPayableWith(ManaAtom.fromName(color), ai.getManaPool())) {
if (satisfiesColorChoice(abMana, choiceString, choice) && testCost.needsColor(ManaAtom.fromName(color), ai.getManaPool())) {
payMultipleMana(testCost, color, ai);
if (nMana != 1) {
choiceString.append(" ");
@@ -1880,7 +1878,7 @@ public class ComputerUtilMana {
final Card offering = sa.getSacrificedAsOffering();
offering.setUsedToPay(false);
if (costIsPaid && !test) {
sa.getHostCard().getGame().getAction().sacrifice(offering, sa, null);
sa.getHostCard().getGame().getAction().sacrifice(offering, sa, null, null);
}
sa.resetSacrificedAsOffering();
}
@@ -1888,7 +1886,7 @@ public class ComputerUtilMana {
final Card emerge = sa.getSacrificedAsEmerge();
emerge.setUsedToPay(false);
if (costIsPaid && !test) {
sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, null);
sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, null, null);
}
sa.resetSacrificedAsEmerge();
}

View File

@@ -49,6 +49,7 @@ public abstract class GameState {
ZONES.put(ZoneType.Library, "library");
ZONES.put(ZoneType.Exile, "exile");
ZONES.put(ZoneType.Command, "command");
ZONES.put(ZoneType.Sideboard, "sideboard");
}
private int humanLife = -1;
@@ -398,6 +399,12 @@ public abstract class GameState {
// Need to figure out a better way to detect if it's actually on adventure.
newText.append("|OnAdventure");
}
if (c.isForetold()) {
newText.append("|Foretold");
}
if (c.isForetoldThisTurn()) {
newText.append("|ForetoldThisTurn");
}
}
@@ -565,6 +572,13 @@ public abstract class GameState {
aiCardTexts.put(ZoneType.Command, categoryValue);
}
else if (categoryName.endsWith("sideboard")) {
if (isHuman)
humanCardTexts.put(ZoneType.Sideboard, categoryValue);
else
aiCardTexts.put(ZoneType.Sideboard, categoryValue);
}
else if (categoryName.startsWith("ability")) {
abilityString.put(categoryName.substring("ability".length()), categoryValue);
}
@@ -1175,7 +1189,7 @@ public abstract class GameState {
String[] allCounterStrings = counterString.split(",");
for (final String counterPair : allCounterStrings) {
String[] pair = counterPair.split("=", 2);
entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, null, false, false, null);
}
}
@@ -1391,6 +1405,14 @@ public abstract class GameState {
}
} else if (info.equals("NoETBTrigs")) {
cardsWithoutETBTrigs.add(c);
} else if (info.equals("Foretold")) {
c.setForetold(true);
c.turnFaceDown(true);
c.addMayLookTemp(c.getOwner());
} else if (info.equals("ForetoldThisTurn")) {
c.setForetoldThisTurn(true);
} else if (info.equals("IsToken")) {
c.setToken(true);
}
}

View File

@@ -137,6 +137,23 @@ public class PlayerControllerAi extends PlayerController {
return new HashMap<>();
}
@Override
public Map<Byte, Integer> specifyManaCombo(SpellAbility sa, ColorSet colorSet, int manaAmount, boolean different) {
Map<Byte, Integer> result = new HashMap<>();
for (int i = 0; i < manaAmount; ++i) {
Byte chosen = chooseColor("", sa, colorSet);
if (result.containsKey(chosen)) {
result.put(chosen, result.get(chosen) + 1);
} else {
result.put(chosen, 1);
}
if (different) {
colorSet = ColorSet.fromMask(colorSet.getColor() - chosen);
}
}
return result;
}
@Override
public Integer announceRequirements(SpellAbility ability, String announce) {
// For now, these "announcements" are made within the AI classes of the appropriate SA effects

View File

@@ -217,7 +217,7 @@ public class SpecialCardAi {
Card animated = AnimateAi.becomeAnimated(sa.getHostCard(), sa.getSubAbility());
if (sa.getHostCard().canReceiveCounters(CounterEnumType.P1P1)) {
animated.addCounter(CounterEnumType.P1P1, 2, ai, false, null);
animated.addCounter(CounterEnumType.P1P1, 2, ai, sa.getSubAbility(), false, null);
}
boolean isOppEOT = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);

View File

@@ -290,7 +290,6 @@ public abstract class SpellAbilityAi {
return false;
}
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
}

View File

@@ -171,6 +171,7 @@ public enum SpellApiToAi {
.put(ApiType.UnattachAll, UnattachAllAi.class)
.put(ApiType.Untap, UntapAi.class)
.put(ApiType.UntapAll, UntapAllAi.class)
.put(ApiType.Venture, VentureAi.class)
.put(ApiType.Vote, VoteAi.class)
.put(ApiType.WinsGame, GameWinAi.class)

View File

@@ -984,7 +984,7 @@ public class AttachAi extends SpellAbilityAi {
List<GameObject> targets = new ArrayList<>();
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
targets = AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa);
targets = AbilityUtils.getDefinedObjects(card, sa.getParam("Defined"), sa);
} else {
AttachAi.attachPreference(sa, tgt, mandatory);
targets = sa.getTargets();
@@ -1344,7 +1344,7 @@ public class AttachAi extends SpellAbilityAi {
CardCollection list = null;
if (tgt == null) {
list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
list = AbilityUtils.getDefinedCards(attachSource, sa.getParam("Defined"), sa);
} else {
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);

View File

@@ -1709,7 +1709,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
return true;
}
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Called when looking for creature to attach aura or equipment

View File

@@ -23,21 +23,23 @@ import forge.util.collect.FCollection;
public class CharmAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
// sa is Entwined, no need for extra logic
if (sa.isEntwine()) {
return true;
}
final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
final int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
final int num;
final int min;
if (sa.isEntwine()) {
num = min = choices.size();
}
else {
num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
}
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
// Reset the chosen list otherwise it will be locked in forever by earlier calls
sa.setChosenList(null);
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
List<AbilitySub> chosenList;
if (!ai.equals(sa.getActivatingPlayer())) {
@@ -159,7 +161,7 @@ public class CharmAi extends SpellAbilityAi {
chosenList.add(allyTainted ? gain : lose);
} else if (oppTainted || ai.getGame().isCardInPlay("Rain of Gore")) {
// Rain of Gore does negate lifegain, so don't benefit the others
// same for if a oppoent does control Tainted Remedy
// same for if a opponent does control Tainted Remedy
// but if ai cant gain life, the effects are negated
chosenList.add(ai.canGainLife() ? lose : gain);
} else if (ai.getGame().isCardInPlay("Sulfuric Vortex")) {
@@ -177,13 +179,13 @@ public class CharmAi extends SpellAbilityAi {
chosenList.add(gain);
} else if(!ai.canGainLife() && aiLife == 14 ) {
// ai cant gain life, but try to avoid falling to 13
// but if a oppoent does control Tainted Remedy its irrelevant
// but if a opponent does control Tainted Remedy its irrelevant
chosenList.add(oppTainted ? lose : gain);
} else if (allyTainted) {
// Tainted Remedy negation logic, try gain instead of lose
// because negation does turn it into lose for opponents
boolean oppCritical = false;
// an oppoent is Critical = 14, and can't gain life, try to lose life instead
// an opponent is Critical = 14, and can't gain life, try to lose life instead
// but only if ai doesn't kill itself with that.
if (aiLife != 14) {
for (Player p : opponents) {
@@ -197,7 +199,7 @@ public class CharmAi extends SpellAbilityAi {
} else {
// normal logic, try to gain life if its critical
boolean oppCritical = false;
// an oppoent is Critical = 12, and can gain life, try to gain life instead
// an opponent is Critical = 12, and can gain life, try to gain life instead
// but only if ai doesn't kill itself with that.
if (aiLife != 12) {
for (Player p : opponents) {
@@ -224,6 +226,8 @@ public class CharmAi extends SpellAbilityAi {
goodChoice = sub;
} else {
// Standard canPlayAi()
sub.setActivatingPlayer(ai);
sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone());
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub);
if (chosenList.size() == min) {

View File

@@ -246,7 +246,6 @@ public class ChooseCardAi extends SpellAbilityAi {
return true;
}
});
System.out.println("Tangle Wire" + options + " - " + betterList);
if (!betterList.isEmpty()) {
choice = betterList.get(0);
} else {

View File

@@ -68,7 +68,6 @@ import forge.util.Aggregates;
public class ControlGainAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
final List<String> lose = Lists.newArrayList();
if (sa.hasParam("LoseControl")) {

View File

@@ -29,7 +29,6 @@ import forge.util.collect.FCollection;
public class CountersMoveAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) {
sa.resetTargets();
if (!moveTgtAI(ai, sa)) {
@@ -83,8 +82,7 @@ public class CountersMoveAi extends SpellAbilityAi {
return true;
}
// something you can't block, try to reduce its
// attack
// something you can't block, try to reduce its attack
if (!ComputerUtilCard.canBeBlockedProfitably(ai, cpy)) {
return true;
}
@@ -119,7 +117,6 @@ public class CountersMoveAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -237,7 +234,6 @@ public class CountersMoveAi extends SpellAbilityAi {
}
private boolean moveTgtAI(final Player ai, final SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = ai.getGame();
final String type = sa.getParam("CounterType");
@@ -283,8 +279,7 @@ public class CountersMoveAi extends SpellAbilityAi {
// cant use substract on Copy
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
// do not steal a P1P1 from Undying if it would die
// this way
// do not steal a P1P1 from Undying if it would die this way
if (cType != null && cType.is(CounterEnumType.P1P1) && srcCardCpy.getNetToughness() <= 0) {
return srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken();
}
@@ -373,8 +368,13 @@ public class CountersMoveAi extends SpellAbilityAi {
}
Card lki = CardUtil.getLKICopy(src);
if (cType == null) {
lki.clearCounters();
// go for opponent when value implies debuff
}
else {
lki.setCounters(cType, 0);
}
// go for opponent when higher value implies debuff
if (ComputerUtilCard.evaluateCreature(src) > ComputerUtilCard.evaluateCreature(lki)) {
List<Card> aiList = CardLists.filterControlledBy(tgtCards, ai);
if (!aiList.isEmpty()) {
@@ -419,6 +419,12 @@ public class CountersMoveAi extends SpellAbilityAi {
return true;
}
}
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (!isMandatoryTrigger) {
// no good target
return false;
}
}
// move counter to opponents creature but only if you can not steal them

View File

@@ -64,11 +64,9 @@ public class CountersPutAi extends SpellAbilityAi {
*/
@Override
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
final String type = sa.getParam("CounterType");
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// TODO Auto-generated method stub
if (!super.willPayCosts(ai, sa, cost, source)) {
return false;
}
@@ -225,8 +223,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
if (sa.canTarget(ai)) {
// don't target itself when its forced to add poison
// counters too
// don't target itself when its forced to add poison counters too
if (!ai.getCounters().isEmpty()) {
if (!eachExisting || ai.getPoisonCounters() < 5) {
sa.getTargets().add(ai);
@@ -480,7 +477,6 @@ public class CountersPutAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
// don't put the counter on the dead creature
if (sacSelf && c.equals(source)) {
return false;
@@ -493,6 +489,8 @@ public class CountersPutAi extends SpellAbilityAi {
Card sacTarget = ComputerUtil.getCardPreference(ai, source, "SacCost", list);
// this card is planned to be sacrificed during cost payment, so don't target it
// (otherwise the AI can cheat by activating this SA and not paying the sac cost, e.g. Extruder)
// TODO needs update if amount > 1 gets printed,
// maybe also check putting the counter on that exact creature is more important than sacrificing it (though unlikely?)
list.remove(sacTarget);
}

View File

@@ -195,7 +195,6 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
*/
@Override
public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, Map<String, Object> params) {
if (options.size() > 1) {
final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();

View File

@@ -81,7 +81,6 @@ public class CountersRemoveAi extends SpellAbilityAi {
*/
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final String type = sa.getParam("CounterType");
if (sa.usesTargeting()) {

View File

@@ -46,7 +46,7 @@ public class DamageAllAi extends SpellAbilityAi {
int x = -1;
final String damage = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
if (damage.equals("X") && sa.getSVar(damage).equals("Count$Converge")) {
dmg = ComputerUtilMana.getConvergeCount(sa, ai);
}
@@ -202,7 +202,7 @@ public class DamageAllAi extends SpellAbilityAi {
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(dmg);
} else {
dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
dmg = AbilityUtils.calculateAmount(source, damage, sa);
}
if (sa.hasParam("ValidPlayers")) {
@@ -286,7 +286,7 @@ public class DamageAllAi extends SpellAbilityAi {
dmg = ComputerUtilCost.getMaxXValue(sa, ai);
sa.setXManaCostPaid(dmg);
} else {
dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
dmg = AbilityUtils.calculateAmount(source, damage, sa);
}
if (sa.hasParam("ValidPlayers")) {

View File

@@ -35,11 +35,11 @@ import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.cost.Cost;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
@@ -53,10 +53,9 @@ public class DamageDealAi extends DamageAiBase {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final String damage = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
final String logic = sa.getParam("AILogic");
Card source = sa.getHostCard();
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
final String logic = sa.getParam("AILogic");
if ("MadSarkhanDigDmg".equals(logic)) {
return SpecialCardAi.SarkhanTheMad.considerDig(ai, sa);
@@ -101,13 +100,12 @@ public class DamageDealAi extends DamageAiBase {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String damage = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
if (damage.equals("X")) {
if (sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
@@ -439,63 +437,12 @@ public class DamageDealAi extends DamageAiBase {
// We can hurt a planeswalker, so rank the one which is the best target
if (!hPlay.isEmpty() && pl.isOpponentOf(ai) && activator.equals(ai)) {
return getBestPlaneswalkerToDamage(hPlay);
return ComputerUtilCard.getBestPlaneswalkerToDamage(hPlay);
}
return null;
}
private Card getBestPlaneswalkerToDamage(final List<Card> pws) {
Card bestTgt = null;
// As of right now, ranks planeswalkers by their Current Loyalty * 10 + Big buff if close to "Ultimate"
int bestScore = 0;
for (Card pw : pws) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
int pwScore = curLoyalty * 10;
for (SpellAbility sa : pw.getSpellAbilities()) {
if (sa.hasParam("Ultimate")) {
Integer loyaltyCost = 0;
CostRemoveCounter remLoyalty = sa.getPayCosts().getCostPartByType(CostRemoveCounter.class);
if (remLoyalty != null) {
// if remLoyalty is null, generally there's an AddCounter<0/LOYALTY> cost, like for Gideon Jura.
loyaltyCost = remLoyalty.convertAmount();
}
if (loyaltyCost != null && loyaltyCost != 0 && loyaltyCost - curLoyalty <= 1) {
// Will ultimate soon
pwScore += 10000;
}
if (pwScore > bestScore) {
bestScore = pwScore;
bestTgt = pw;
}
}
}
}
return bestTgt;
}
private Card getWorstPlaneswalkerToDamage(final List<Card> pws) {
Card bestTgt = null;
int bestScore = Integer.MAX_VALUE;
for (Card pw : pws) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
if (curLoyalty < bestScore) {
bestScore = curLoyalty;
bestTgt = pw;
}
}
return bestTgt;
}
private List<Card> getTargetableCards(Player ai, SpellAbility sa, Player pl, TargetRestrictions tgt, Player activator, Card source, Game game) {
List<Card> hPlay = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), activator, source, sa);
@@ -584,7 +531,7 @@ public class DamageDealAi extends DamageAiBase {
&& "P1P1".equals(sa.getParent().getParam("CounterType"))) {
// assuming the SA parent is of PutCounter type. Perhaps it's possible to predict counter multipliers here somehow?
final String amountStr = sa.getParent().getParamOrDefault("CounterNum", "1");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
dmg += amount;
}
@@ -897,7 +844,7 @@ public class DamageDealAi extends DamageAiBase {
// this is for Triggered targets that are mandatory
final boolean noPrevention = sa.hasParam("NoPrevention");
final boolean divided = sa.isDividedAsYouChoose();
final Player opp = ai.getWeakestOpponent();
PlayerCollection opps = ai.getOpponents();
while (sa.canAddMoreTarget()) {
if (tgt.canTgtPlaneswalker()) {
@@ -925,6 +872,9 @@ public class DamageDealAi extends DamageAiBase {
}
}
if (!opps.isEmpty()) {
Player opp = opps.getFirst();
opps.remove(opp);
if (sa.canTarget(opp)) {
if (sa.getTargets().add(opp)) {
if (divided) {
@@ -934,6 +884,7 @@ public class DamageDealAi extends DamageAiBase {
continue;
}
}
}
// See if there's an indestructible target that can be used
CardCollection indestructible = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
@@ -950,7 +901,7 @@ public class DamageDealAi extends DamageAiBase {
}
else if (tgt.canTgtPlaneswalker()) {
// Second pass for planeswalkers: choose AI's worst planeswalker
final Card c = getWorstPlaneswalkerToDamage(CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.PLANESWALKERS), CardPredicates.isTargetableBy(sa)));
final Card c = ComputerUtilCard.getWorstPlaneswalkerToDamage(CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.PLANESWALKERS), CardPredicates.isTargetableBy(sa)));
if (c != null) {
sa.getTargets().add(c);
if (divided) {

View File

@@ -24,7 +24,7 @@ import forge.game.zone.ZoneType;
public class DestroyAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return canPlayAI(ai, sa);
return checkApiLogic(ai, sa);
}
@Override

View File

@@ -244,7 +244,7 @@ public class DrawAi extends SpellAbilityAi {
int numCards = 1;
if (sa.hasParam("NumCards")) {
numCards = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCards"), sa);
numCards = AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa);
}
boolean xPaid = false;

View File

@@ -67,8 +67,7 @@ public class FightAi extends SpellAbilityAi {
for (Card humanCreature : humCreatures) {
if (ComputerUtilCombat.getDamageToKill(humanCreature) <= fighter1.getNetPower()
&& humanCreature.getNetPower() < ComputerUtilCombat.getDamageToKill(fighter1)) {
// todo: check min/max targets; see if we picked the best
// matchup
// todo: check min/max targets; see if we picked the best matchup
sa.getTargets().add(humanCreature);
return true;
} else if (humanCreature.getSVar("Targeting").equals("Dies")) {
@@ -85,8 +84,7 @@ public class FightAi extends SpellAbilityAi {
for (Card aiCreature : aiCreatures) {
if (ComputerUtilCombat.getDamageToKill(humanCreature) <= aiCreature.getNetPower()
&& humanCreature.getNetPower() < ComputerUtilCombat.getDamageToKill(aiCreature)) {
// todo: check min/max targets; see if we picked the
// best matchup
// todo: check min/max targets; see if we picked the best matchup
sa.getTargets().add(humanCreature);
sa.getTargets().add(aiCreature);
return true;

View File

@@ -114,7 +114,7 @@ public class LifeSetAi extends SpellAbilityAi {
sa.setXManaCostPaid(xPay);
amount = xPay;
} else {
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
amount = AbilityUtils.calculateAmount(source, amountStr, sa);
}
// special cases when amount can't be calculated without targeting first

View File

@@ -78,7 +78,7 @@ public class MustBlockAi extends SpellAbilityAi {
Card attacker = null;
if (sa.hasParam("DefinedAttacker")) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DefinedAttacker"), sa);
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedAttacker"), sa);
if (cards.isEmpty()) {
return false;
}

View File

@@ -33,7 +33,6 @@ public class PermanentAi extends SpellAbilityAi {
*/
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final Card card = sa.getHostCard();
if (card.hasKeyword("MayFlashSac") && !ai.couldCastSorcery(sa)) {
@@ -51,7 +50,6 @@ public class PermanentAi extends SpellAbilityAi {
*/
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard();
final Game game = ai.getGame();

View File

@@ -58,7 +58,6 @@ public class PermanentCreatureAi extends PermanentAi {
*/
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final Card card = sa.getHostCard();
final Game game = ai.getGame();
@@ -176,7 +175,6 @@ public class PermanentCreatureAi extends PermanentAi {
}
}
if (hasFloatMana || willDiscardNow || willDieNow) {
// Will lose mana in pool or about to discard a card in cleanup or about to die in combat, so use this opportunity
return true;
@@ -207,7 +205,6 @@ public class PermanentCreatureAi extends PermanentAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
if (!super.checkApiLogic(ai, sa)) {
return false;
}

View File

@@ -27,7 +27,6 @@ public class PermanentNoncreatureAi extends PermanentAi {
*/
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (!super.checkApiLogic(ai, sa))
return false;

View File

@@ -28,6 +28,7 @@ import forge.game.card.CardPredicates;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
@@ -63,7 +64,7 @@ public class PlayAi extends SpellAbilityAi {
return false;
}
} else if (!sa.hasParam("Valid")) {
cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if (cards.isEmpty()) {
return false;
}
@@ -158,6 +159,11 @@ public class PlayAi extends SpellAbilityAi {
return true;
}
@Override
public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@@ -192,7 +198,7 @@ public class PlayAi extends SpellAbilityAi {
spell = (Spell) spell.copyWithDefinedCost(abCost);
}
if (AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) {
if (AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !(isOptional || sa.hasParam("Optional")), true)) {
// Before accepting, see if the spell has a valid number of targets (it should at this point).
// Proceeding past this point if the spell is not correctly targeted will result
// in "Failed to add to stack" error and the card disappearing from the game completely.

View File

@@ -202,11 +202,10 @@ public class ProtectAi extends SpellAbilityAi {
sa.resetTargets();
CardCollection list = getProtectCreatures(ai, sa);
list = CardLists.getValidCards(list, tgt.getValidTgts(), sa.getActivatingPlayer(), sa.getHostCard(), sa);
list = CardLists.getValidCards(list, tgt.getValidTgts(), sa.getActivatingPlayer(), source, sa);
if (game.getStack().isEmpty()) {
// If the cost is tapping, don't activate before declare
// attack/block
// If the cost is tapping, don't activate before declare attack/block
if (sa.getPayCosts().hasTapCost()) {
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().isPlayerTurn(ai)) {
@@ -341,7 +340,7 @@ public class ProtectAi extends SpellAbilityAi {
sa.getTargets().add(c);
}
if (sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (sa.getTargets().size() < tgt.getMinTargets(source, sa)) {
sa.resetTargets();
return false;
}

View File

@@ -382,8 +382,7 @@ public class PumpAi extends PumpAiBase {
return false;
}
// when this happens we need to expand AI to consider if its ok for
// everything?
// when this happens we need to expand AI to consider if its ok for everything?
for (final Card card : cards) {
if (sa.isCurse()) {
if (!card.getController().isOpponentOf(ai)) {
@@ -488,7 +487,31 @@ public class PumpAi extends PumpAiBase {
// each player sacrifices one permanent, e.g. Vaevictis, Asmadi the Dire - grab the worst for allied and
// the best for opponents
return SacrificeAi.doSacOneEachLogic(ai, sa);
} else if (sa.getParam("AILogic").equals("Destroy")) {
List<Card> tgts = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
if (tgts.isEmpty()) {
return false;
}
List<Card> alliedTgts = CardLists.filter(tgts, Predicates.or(CardPredicates.isControlledByAnyOf(ai.getAllies()), CardPredicates.isController(ai)));
List<Card> oppTgts = CardLists.filter(tgts, CardPredicates.isControlledByAnyOf(ai.getRegisteredOpponents()));
Card destroyTgt = null;
if (!oppTgts.isEmpty()) {
destroyTgt = ComputerUtilCard.getBestAI(oppTgts);
} else {
// TODO: somehow limit this so that the AI doesn't always destroy own stuff when able?
destroyTgt = ComputerUtilCard.getWorstAI(alliedTgts);
}
if (destroyTgt != null) {
sa.getTargets().add(destroyTgt);
return true;
}
return false;
}
if (isFight) {
return FightAi.canFightAi(ai, sa, attack, defense);
}
@@ -517,8 +540,7 @@ public class PumpAi extends PumpAiBase {
list = CardLists.getValidCards(list, tgt.getValidTgts(), ai, source, sa);
if (game.getStack().isEmpty()) {
// If the cost is tapping, don't activate before declare
// attack/block
// If the cost is tapping, don't activate before declare attack/block
if (sa.getPayCosts().hasTapCost()) {
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().isPlayerTurn(ai)) {

View File

@@ -61,20 +61,20 @@ public class PumpAllAi extends PumpAiBase {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = ai.getStrongestOpponent();
if (tgt != null && sa.canTarget(opp) && sa.hasParam("IsCurse")) {
if (tgt != null && sa.canTarget(opp) && sa.isCurse()) {
sa.resetTargets();
sa.getTargets().add(opp);
return true;
}
if (tgt != null && sa.canTarget(ai) && !sa.hasParam("IsCurse")) {
if (tgt != null && sa.canTarget(ai) && !sa.isCurse()) {
sa.resetTargets();
sa.getTargets().add(ai);
return true;
}
final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa);
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
final int power = AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa);
final int defense = AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa);
final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>();
final PhaseType phase = game.getPhaseHandler().getPhase();
@@ -88,7 +88,7 @@ public class PumpAllAi extends PumpAiBase {
if (!game.getStack().isEmpty() && !sa.isCurse()) {
return pumpAgainstRemoval(ai, sa, comp);
}
if (sa.hasParam("IsCurse")) {
if (sa.isCurse()) {
if (defense < 0) { // try to destroy creatures
comp = CardLists.filter(comp, new Predicate<Card>() {
@Override

View File

@@ -23,7 +23,6 @@ public class SacrificeAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
return sacrificeTgtAI(ai, sa);
}

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -8,16 +9,33 @@ import forge.game.spellability.SpellAbility;
public class SkipPhaseAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
return targetPlayer(aiPlayer, sa, false);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
return targetPlayer(aiPlayer, sa, mandatory);
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
public boolean targetPlayer(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
sa.resetTargets();
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
}
else if (mandatory && sa.canTarget(ai)) {
sa.getTargets().add(ai);
}
else {
return false;
}
}
return true;
}
}

View File

@@ -1,6 +1,5 @@
package forge.ai.ability;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;

View File

@@ -50,7 +50,6 @@ public class TokenAi extends SpellAbilityAi {
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final Card source = sa.getHostCard();
// Planeswalker-related flags
boolean pwMinus = false;

View File

@@ -0,0 +1,75 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.AiPlayDecision;
import forge.ai.AiProps;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.card.ICardFace;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import java.util.List;
import java.util.Map;
public class VentureAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
// TODO: is it ever a bad idea to venture into a dungeon?
return true;
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
// AI that handles choosing the next room in a dungeon
@Override
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
List<SpellAbility> viableRooms = Lists.newArrayList();
for (SpellAbility room : spells) {
if (player.getController().isAI()) { // FIXME: is this needed? Can simulation ever run this for a non-AI player?
if (((PlayerControllerAi)player.getController()).getAi().canPlaySa(room) == AiPlayDecision.WillPlay) {
viableRooms.add(room);
}
}
}
if (!viableRooms.isEmpty()) {
// choose a room at random from the ones that are deemed playable
return Aggregates.random(viableRooms);
}
return Aggregates.random(spells); // If we're here, we should choose at least something, so choose a random thing then
}
// AI that chooses which dungeon to venture into
@Override
public String chooseCardName(Player ai, SpellAbility sa, List<ICardFace> faces) {
// TODO: improve the conditions that define which dungeon is a viable option to choose
List<String> dungeonNames = Lists.newArrayList();
for (ICardFace face : faces) {
dungeonNames.add(face.getName());
}
// Don't choose Tomb of Annihilation when life in danger unless we can win right away or can't lose for 0 life
if (ai.getController().isAI()) { // FIXME: is this needed? Can simulation ever run this for a non-AI player?
int lifeInDanger = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
if ((ai.getLife() <= lifeInDanger && !ai.cantLoseForZeroOrLessLife())
&& !(ai.getLife() > 1 && ai.getWeakestOpponent().getLife() == 1)) {
dungeonNames.remove("Tomb of Annihilation");
}
}
return Aggregates.random(dungeonNames);
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.40-SNAPSHOT</version>
<version>1.6.43-SNAPSHOT</version>
</parent>
<artifactId>forge-core</artifactId>

View File

@@ -167,6 +167,22 @@ public class StaticData {
return sortedEditions;
}
private TreeMap<CardEdition.Type, List<CardEdition>> editionsTypeMap;
public final Map<CardEdition.Type, List<CardEdition>> getEditionsTypeMap(){
if (editionsTypeMap == null){
editionsTypeMap = new TreeMap<>();
for (CardEdition.Type editionType : CardEdition.Type.values()){
editionsTypeMap.put(editionType, new ArrayList<>());
}
for (CardEdition edition : this.getSortedEditions()){
CardEdition.Type key = edition.getType();
List<CardEdition> editionsOfType = editionsTypeMap.get(key);
editionsOfType.add(edition);
}
}
return editionsTypeMap;
}
public CardEdition getCardEdition(String setCode){
CardEdition edition = this.editions.get(setCode);
if (edition == null) // try custom editions

View File

@@ -716,7 +716,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} catch (Exception ex) {
return false;
}
return edition != null && edition.getType() != Type.PROMOS;
return edition != null && edition.getType() != Type.PROMO;
}
}));
}
@@ -728,7 +728,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
CardEdition edition = null;
try {
edition = editions.getEditionByCodeOrThrow(paperCard.getEdition());
if (edition.getType() == Type.PROMOS || edition.getType() == Type.REPRINT)
if (edition.getType() == Type.PROMO||edition.getType() == Type.REPRINT)
return false;
} catch (Exception ex) {
return false;

View File

@@ -56,19 +56,23 @@ public final class CardEdition implements Comparable<CardEdition> {
CORE,
EXPANSION,
REPRINT,
ONLINE,
STARTER,
REPRINT,
BOXED_SET,
DUEL_DECKS,
PREMIUM_DECK_SERIES,
FROM_THE_VAULT,
COLLECTOR_EDITION,
DUEL_DECK,
PROMO,
ONLINE,
OTHER,
PROMOS,
DRAFT,
COMMANDER,
MULTIPLAYER,
FUNNY,
THIRDPARTY; // custom sets
OTHER, // FALLBACK CATEGORY
CUSTOM_SET; // custom sets
public String getBoosterBoxDefault() {
switch (this) {
@@ -79,6 +83,19 @@ public final class CardEdition implements Comparable<CardEdition> {
return "0";
}
}
public String toString(){
String[] names = TextUtil.splitWithParenthesis(this.name().toLowerCase(), '_');
for (int i = 0; i < names.length; i++)
names[i] = TextUtil.capitalize(names[i]);
return TextUtil.join(Arrays.asList(names), " ");
}
public static Type fromString(String label){
List<String> names = Arrays.asList(TextUtil.splitWithParenthesis(label.toUpperCase(), ' '));
String value = TextUtil.join(names, "_");
return Type.valueOf(value);
}
}
public enum FoilType {
@@ -103,6 +120,7 @@ public final class CardEdition implements Comparable<CardEdition> {
SPECIAL_SLOT("special slot"), //to help with convoluted boosters
PRECON_PRODUCT("precon product"),
BORDERLESS("borderless"),
ETCHED("etched"),
SHOWCASE("showcase"),
EXTENDED_ART("extended art"),
ALTERNATE_ART("alternate art"),
@@ -247,6 +265,7 @@ public final class CardEdition implements Comparable<CardEdition> {
private int boosterArts = 1;
private SealedProduct.Template boosterTpl = null;
private final Map<String, SealedProduct.Template> boosterTemplates = new HashMap<>();
private CardEdition(ListMultimap<String, CardInSet> cardMap, Map<String, Integer> tokens, Map<String, List<String>> customPrintSheetsToParse) {
this.cardMap = cardMap;
@@ -393,11 +412,28 @@ public final class CardEdition implements Comparable<CardEdition> {
}
public SealedProduct.Template getBoosterTemplate() {
return boosterTpl;
return getBoosterTemplate("Draft");
}
public SealedProduct.Template getBoosterTemplate(String boosterType) {
return boosterTemplates.get(boosterType);
}
public String getRandomBoosterKind() {
List<String> boosterTypes = Lists.newArrayList(boosterTemplates.keySet());
if (boosterTypes.isEmpty()) {
return null;
}
Collections.shuffle(boosterTypes);
return boosterTypes.get(0);
}
public Set<String> getAvailableBoosterTypes() {
return boosterTemplates.keySet();
}
public boolean hasBoosterTemplate() {
return boosterTpl != null;
return boosterTemplates.containsKey("Draft");
}
public List<PrintSheet> getPrintSheetsBySection() {
@@ -435,8 +471,16 @@ public final class CardEdition implements Comparable<CardEdition> {
}
public static class Reader extends StorageReaderFolder<CardEdition> {
private boolean isCustomEditions;
public Reader(File path) {
super(path, CardEdition.FN_GET_CODE);
this.isCustomEditions = false;
}
public Reader(File path, boolean isCustomEditions) {
super(path, CardEdition.FN_GET_CODE);
this.isCustomEditions = isCustomEditions;
}
@Override
@@ -537,12 +581,29 @@ public final class CardEdition implements Comparable<CardEdition> {
res.boosterArts = section.getInt("BoosterCovers", 1);
String boosterDesc = section.get("Booster");
res.boosterTpl = boosterDesc == null ? null : new SealedProduct.Template(res.code, SealedProduct.Template.Reader.parseSlots(boosterDesc));
if (section.contains("Booster")) {
// Historical naming convention in Forge for "DraftBooster"
res.boosterTpl = new SealedProduct.Template(res.code, SealedProduct.Template.Reader.parseSlots(boosterDesc));
res.boosterTemplates.put("Draft", res.boosterTpl);
}
String[] boostertype = { "Draft", "Collector", "Set" };
// Theme boosters aren't here because they are closer to preconstructed decks, and should be treated as such
for (String type : boostertype) {
String name = type + "Booster";
if (section.contains(name)) {
res.boosterTemplates.put(type, new SealedProduct.Template(res.code, SealedProduct.Template.Reader.parseSlots(section.get(name))));
}
}
res.alias = section.get("alias");
res.borderColor = BorderColor.valueOf(section.get("border", "Black").toUpperCase(Locale.ENGLISH));
String type = section.get("type");
Type enumType = Type.UNKNOWN;
if (this.isCustomEditions){
enumType = Type.CUSTOM_SET; // Forcing ThirdParty Edition Type to avoid inconsistencies
} else {
String type = section.get("type");
if (null != type && !type.isEmpty()) {
try {
enumType = Type.valueOf(type.toUpperCase(Locale.ENGLISH));
@@ -551,6 +612,8 @@ public final class CardEdition implements Comparable<CardEdition> {
System.err.println("Ignoring unknown type in set definitions: name: " + res.name + "; type: " + type);
}
}
}
res.type = enumType;
res.prerelease = section.get("Prerelease", null);
res.boosterBoxCount = Integer.parseInt(section.get("BoosterBox", enumType.getBoosterBoxDefault()));
@@ -684,14 +747,16 @@ public final class CardEdition implements Comparable<CardEdition> {
};
public IItemReader<SealedProduct.Template> getBoosterGenerator() {
// TODO Auto-generated method stub
return new StorageReaderBase<SealedProduct.Template>(null) {
@Override
public Map<String, SealedProduct.Template> readAll() {
Map<String, SealedProduct.Template> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for(CardEdition ce : Collection.this) {
if (ce.hasBoosterTemplate()) {
map.put(ce.getCode(), ce.getBoosterTemplate());
List<String> boosterTypes = Lists.newArrayList(ce.getAvailableBoosterTypes());
for (String type : boosterTypes) {
String setAffix = type.equals("Draft") ? "" : type;
map.put(ce.getCode() + setAffix, ce.getBoosterTemplate(type));
}
}
return map;

View File

@@ -116,7 +116,7 @@ public final class CardRules implements ICardCharacteristics {
public boolean isVariant() {
CardType t = getType();
return t.isVanguard() || t.isScheme() || t.isPlane() || t.isPhenomenon() || t.isConspiracy();
return t.isVanguard() || t.isScheme() || t.isPlane() || t.isPhenomenon() || t.isConspiracy() || t.isDungeon();
}
public CardSplitType getSplitType() {
@@ -211,11 +211,20 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean canBeCommander() {
CardType type = mainPart.getType();
if (type.isLegendary() && type.isCreature()) {
if (mainPart.getOracleText().contains("can be your commander")) {
return true;
}
return mainPart.getOracleText().contains("can be your commander");
CardType type = mainPart.getType();
boolean creature = type.isCreature();
for (String staticAbility : mainPart.getStaticAbilities()) { // Check for Grist
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
creature = true;
}
}
if (type.isLegendary() && creature) {
return true;
}
return false;
}
public boolean canBePartnerCommander() {
@@ -234,12 +243,38 @@ public final class CardRules implements ICardCharacteristics {
public boolean canBeBrawlCommander() {
CardType type = mainPart.getType();
return type.isLegendary() && (type.isCreature() || type.isPlaneswalker());
if (!type.isLegendary()) {
return false;
}
if (type.isCreature() || type.isPlaneswalker()) {
return true;
}
// Grist is checked above, but new cards might work this way
for (String staticAbility : mainPart.getStaticAbilities()) {
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
return true;
}
}
return false;
}
public boolean canBeTinyLeadersCommander() {
CardType type = mainPart.getType();
return type.isLegendary() && (type.isCreature() || type.isPlaneswalker());
if (!type.isLegendary()) {
return false;
}
if (type.isCreature() || type.isPlaneswalker()) {
return true;
}
// Grist is checked above, but new cards might work this way
for (String staticAbility : mainPart.getStaticAbilities()) {
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
return true;
}
}
return false;
}
public String getMeldWith() {

View File

@@ -598,6 +598,7 @@ public final class CardRulesPredicates {
public static final Predicate<CardRules> IS_SCHEME = CardRulesPredicates.coreType(true, CardType.CoreType.Scheme);
public static final Predicate<CardRules> IS_VANGUARD = CardRulesPredicates.coreType(true, CardType.CoreType.Vanguard);
public static final Predicate<CardRules> IS_CONSPIRACY = CardRulesPredicates.coreType(true, CardType.CoreType.Conspiracy);
public static final Predicate<CardRules> IS_DUNGEON = CardRulesPredicates.coreType(true, CardType.CoreType.Dungeon);
public static final Predicate<CardRules> IS_NON_LAND = CardRulesPredicates.coreType(false, CardType.CoreType.Land);
public static final Predicate<CardRules> CAN_BE_BRAWL_COMMANDER = Predicates.and(Presets.IS_LEGENDARY,
Predicates.or(Presets.IS_CREATURE, Presets.IS_PLANESWALKER));

View File

@@ -57,6 +57,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
Artifact(true, "artifacts"),
Conspiracy(false, "conspiracies"),
Creature(true, "creatures"),
Dungeon(false, "dungeons"),
Emblem(false, "emblems"),
Enchantment(true, "enchantments"),
Instant(false, "instants"),
@@ -446,6 +447,11 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return coreTypes.contains(CoreType.Tribal);
}
@Override
public boolean isDungeon() {
return coreTypes.contains(CoreType.Dungeon);
}
@Override
public String toString() {
if (calculatedType == null) {
@@ -686,13 +692,11 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
}
private static boolean isMultiwordType(final String type) {
final String[] multiWordTypes = { "Serra's Realm", "Bolas's Meditation Realm" };
// no need to loop for only 2 exceptions!
if (multiWordTypes[0].startsWith(type) && !multiWordTypes[0].equals(type)) {
final String[] multiWordTypes = { "Serra's Realm", "Bolas's Meditation Realm", "Dungeon Master" };
for (int i = 0; i < multiWordTypes.length; ++i) {
if (multiWordTypes[i].startsWith(type) && !multiWordTypes[i].equals(type)) {
return true;
}
if (multiWordTypes[1].startsWith(type) && !multiWordTypes[1].equals(type)) {
return true;
}
return false;
}

View File

@@ -44,5 +44,6 @@ public interface CardTypeView extends Iterable<String>, Serializable {
boolean isPhenomenon();
boolean isEmblem();
boolean isTribal();
boolean isDungeon();
CardTypeView getTypeWithChanges(Iterable<CardChangedType> changedCardTypes);
}

View File

@@ -35,9 +35,12 @@ public class BoosterPack extends SealedProduct {
public static final Function<CardEdition, BoosterPack> FN_FROM_SET = new Function<CardEdition, BoosterPack>() {
@Override
public BoosterPack apply(final CardEdition arg1) {
Template d = StaticData.instance().getBoosters().get(arg1.getCode());
return new BoosterPack(arg1.getName(), d);
public BoosterPack apply(final CardEdition edition) {
String boosterKind = edition.getRandomBoosterKind();
Template d = edition.getBoosterTemplate(boosterKind);
StringBuilder sb = new StringBuilder(edition.getName());
sb.append(" ").append(boosterKind);
return new BoosterPack(sb.toString(), d);
}
};

View File

@@ -132,7 +132,7 @@ public abstract class SealedProduct implements InventoryItemFromSet {
public static class Template {
@SuppressWarnings("unchecked")
public final static Template genericBooster = new Template(null, Lists.newArrayList(
public final static Template genericDraftBooster = new Template(null, Lists.newArrayList(
Pair.of(BoosterSlots.COMMON, 10), Pair.of(BoosterSlots.UNCOMMON, 3),
Pair.of(BoosterSlots.RARE_MYTHIC, 1), Pair.of(BoosterSlots.BASIC_LAND, 1)
));

View File

@@ -236,6 +236,11 @@ public class BoosterGenerator {
String slotType = slot.getLeft(); // add expansion symbol here?
int numCards = slot.getRight();
boolean convertCardFoil = slotType.endsWith("+");
if (convertCardFoil) {
slotType = slotType.substring(0, slotType.length() - 1);
}
String[] sType = TextUtil.splitWithParenthesis(slotType, ' ');
String setCode = sType.length == 1 && template.getEdition() != null ? template.getEdition() : null;
String sheetKey = StaticData.instance().getEditions().contains(setCode) ? slotType.trim() + " " + setCode
@@ -280,7 +285,19 @@ public class BoosterGenerator {
}
PrintSheet ps = getPrintSheet(sheetKey);
result.addAll(ps.random(numCards, true));
List<PaperCard> paperCards;
// For cards that end in '+', attempt to convert this card to foil.
if (convertCardFoil) {
paperCards = Lists.newArrayList();
for(PaperCard pc : ps.random(numCards, true)) {
paperCards.add(pc.getFoiled());
}
} else {
paperCards = ps.random(numCards, true);
}
result.addAll(paperCards);
sheetsUsed.add(ps);
if (foilInThisSlot) {
@@ -508,6 +525,7 @@ public class BoosterGenerator {
mainCode.regionMatches(true, 0, "wholeSheet", 0, 10)
) { // custom print sheet
String sheetName = StringUtils.strip(mainCode.substring(10), "()\" ");
System.out.println("Attempting to lookup :" + sheetName);
src = StaticData.instance().getPrintSheets().get(sheetName).toFlatList();
setPred = Predicates.alwaysTrue();

View File

@@ -18,7 +18,7 @@ public class TokenDb implements ITokenDatabase {
// colors_power_toughness_cardtypes_sub_types_keywords
// Some examples:
// c_3_3_a_wurm_lifelink
// c_3_3_a_phyrexian_wurm_lifelink
// w_2_2_knight_first_strike
// The image names should be the same as the script name + _set

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.40-SNAPSHOT</version>
<version>1.6.43-SNAPSHOT</version>
</parent>
<artifactId>forge-game</artifactId>

View File

@@ -11,6 +11,7 @@ import forge.game.mana.ManaCostBeingPaid;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
import forge.util.Expressions;
public class ForgeScript {
@@ -122,10 +123,8 @@ public class ForgeScript {
return Expressions.compare(y, property, x);
} else return cardState.getTypeWithChanges().hasStringType(property);
}
public static boolean spellAbilityHasProperty(SpellAbility sa, String property, Player sourceController,
Card source, CardTraitBase spellAbility) {
if (property.equals("ManaAbility")) {
@@ -155,6 +154,8 @@ public class ForgeScript {
return sa.isAftermath();
} else if (property.equals("MorphUp")) {
return sa.isMorphUp();
} else if (property.equals("Modular")) {
return sa.hasParam("Modular");
} else if (property.equals("Equip")) {
return sa.hasParam("Equip");
} else if (property.equals("Boast")) {
@@ -186,7 +187,14 @@ public class ForgeScript {
} else if (property.equals("OppCtrl")) {
return sa.getActivatingPlayer().isOpponentOf(sourceController);
} else if (property.startsWith("cmc")) {
int y = sa.getPayCosts().getTotalMana().getCMC();
int y = 0;
// spell was on the stack
if (sa.getCardState().getCard().isInZone(ZoneType.Stack)) {
y = sa.getHostCard().getCMC();
}
else {
y = sa.getPayCosts().getTotalMana().getCMC();
}
int x = AbilityUtils.calculateAmount(spellAbility.getHostCard(), property.substring(5), spellAbility);
if (!Expressions.compare(y, property, x)) {
return false;

View File

@@ -197,6 +197,15 @@ public class Game {
}
}
public CardCollectionView copyLastStateBattlefield() {
CardCollection result = new CardCollection();
Map<Integer, Card> cachedMap = Maps.newHashMap();
for (final Player p : getPlayers()) {
result.addAll(p.getZone(ZoneType.Battlefield).getLKICopy(cachedMap));
}
return result;
}
public void updateLastStateForCard(Card c) {
if (c == null || c.getZone() == null) {
return;

View File

@@ -251,6 +251,7 @@ public class GameAction {
copied.setChangedCardTraits(c.getChangedCardTraits());
copied.copyChangedTextFrom(c);
copied.setTimestamp(c.getTimestamp());
// copy exiled properties when adding to stack
// will be cleanup later in MagicStack
@@ -672,6 +673,23 @@ public class GameAction {
c = changeZone(zoneFrom, zoneTo, c, position, cause, params);
// need to refresh ability text for affected cards
for (final StaticAbility stAb : c.getStaticAbilities()) {
if (stAb.isSecondary() ||
!stAb.getParam("Mode").equals("CantBlockBy") ||
stAb.isSuppressed() || !stAb.checkConditions() ||
!stAb.hasParam("ValidAttacker") ||
(stAb.hasParam("ValidBlocker") && stAb.getParam("ValidBlocker").equals("Creature.Self"))) {
continue;
}
final Card host = stAb.getHostCard();
for (Card creature : Iterables.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)) {
if (creature.isValid(stAb.getParam("ValidAttacker").split(","), host.getController(), host, stAb)) {
creature.updateAbilityTextForView();
}
}
}
// Move card in maingame if take card from subgame
// 720.4a
if (zoneFrom != null && zoneFrom.is(ZoneType.Sideboard) && game.getMaingame() != null) {
@@ -1175,6 +1193,8 @@ public class GameAction {
for (final Card c : cards) {
// If a token is in a zone other than the battlefield, it ceases to exist.
checkAgain |= stateBasedAction704_5d(c);
// Dungeon Card won't affect other cards, so don't need to set checkAgain
stateBasedAction_Dungeon(c);
}
}
}
@@ -1235,7 +1255,7 @@ public class GameAction {
int loyal = c.getCounters(CounterEnumType.LOYALTY);
if (loyal < beeble) {
GameEntityCounterTable counterTable = new GameEntityCounterTable();
c.addCounter(CounterEnumType.LOYALTY, beeble - loyal, c.getController(), false, counterTable);
c.addCounter(CounterEnumType.LOYALTY, beeble - loyal, c.getController(), null, false, counterTable);
counterTable.triggerCountersPutAll(game);
} else if (loyal > beeble) {
c.subtractCounter(CounterEnumType.LOYALTY, loyal - beeble);
@@ -1261,7 +1281,7 @@ public class GameAction {
orderedNoRegCreats = true;
}
for (Card c : noRegCreats) {
sacrificeDestroy(c, null, table);
sacrificeDestroy(c, null, table, null);
}
}
if (desCreats != null) {
@@ -1273,7 +1293,7 @@ public class GameAction {
orderedDesCreats = true;
}
for (Card c : desCreats) {
destroy(c, null, true, table);
destroy(c, null, true, table, null);
}
}
setHoldCheckingStaticAbilities(false);
@@ -1359,12 +1379,21 @@ public class GameAction {
return false;
}
if (!game.getStack().hasSourceOnStack(c, SpellAbilityPredicates.isChapter())) {
sacrifice(c, null, table);
sacrifice(c, null, table, null);
checkAgain = true;
}
return checkAgain;
}
private void stateBasedAction_Dungeon(Card c) {
if (!c.getType().isDungeon() || !c.isInLastRoom()) {
return;
}
if (!game.getStack().hasSourceOnStack(c, null)) {
completeDungeon(c.getController(), c);
}
}
private boolean stateBasedAction704_attach(Card c, CardZoneTable table) {
boolean checkAgain = false;
@@ -1387,7 +1416,7 @@ public class GameAction {
// cleanup aura
if (c.isAura() && c.isInPlay() && !c.isEnchanting()) {
sacrificeDestroy(c, null, table);
sacrificeDestroy(c, null, table, null);
checkAgain = true;
}
return checkAgain;
@@ -1539,7 +1568,7 @@ public class GameAction {
for (Card c : list) {
if (c.getCounters(CounterEnumType.LOYALTY) <= 0) {
sacrificeDestroy(c, null, table);
sacrificeDestroy(c, null, table, null);
// Play the Destroy sound
game.fireEvent(new GameEventCardDestroyed());
recheck = true;
@@ -1602,7 +1631,7 @@ public class GameAction {
"You have multiple legendary permanents named \""+name+"\" in play.\n\nChoose the one to stay on battlefield (the rest will be moved to graveyard)", null);
for (Card c: cc) {
if (c != toKeep) {
sacrificeDestroy(c, null, table);
sacrificeDestroy(c, null, table, null);
}
}
game.fireEvent(new GameEventCardDestroyed());
@@ -1636,28 +1665,24 @@ public class GameAction {
}
for (Card c : worlds) {
sacrificeDestroy(c, null, table);
sacrificeDestroy(c, null, table, null);
game.fireEvent(new GameEventCardDestroyed());
}
return true;
}
@Deprecated
public final Card sacrifice(final Card c, final SpellAbility source) {
return sacrifice(c, source, null);
}
public final Card sacrifice(final Card c, final SpellAbility source, CardZoneTable table) {
public final Card sacrifice(final Card c, final SpellAbility source, CardZoneTable table, Map<AbilityKey, Object> params) {
if (!c.canBeSacrificedBy(source)) {
return null;
}
c.getController().addSacrificedThisTurn(c, source);
return sacrificeDestroy(c, source, table);
return sacrificeDestroy(c, source, table, params);
}
public final boolean destroy(final Card c, final SpellAbility sa, final boolean regenerate, CardZoneTable table) {
public final boolean destroy(final Card c, final SpellAbility sa, final boolean regenerate, CardZoneTable table, Map<AbilityKey, Object> params) {
Player activator = null;
if (!c.canBeDestroyed()) {
return false;
@@ -1668,6 +1693,9 @@ public class GameAction {
repRunParams.put(AbilityKey.Source, sa);
repRunParams.put(AbilityKey.Affected, c);
repRunParams.put(AbilityKey.Regeneration, regenerate);
if (params != null) {
repRunParams.putAll(params);
}
if (game.getReplacementHandler().run(ReplacementType.Destroy, repRunParams) != ReplacementResult.NotReplaced) {
return false;
@@ -1684,11 +1712,14 @@ public class GameAction {
// Run triggers
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.Causer, activator);
if (params != null) {
runParams.putAll(params);
}
game.getTriggerHandler().runTrigger(TriggerType.Destroyed, runParams, false);
// in case the destroyed card has such a trigger
game.getTriggerHandler().registerActiveLTBTrigger(c);
final Card sacrificed = sacrificeDestroy(c, sa, table);
final Card sacrificed = sacrificeDestroy(c, sa, table, params);
return sacrificed != null;
}
@@ -1696,12 +1727,12 @@ public class GameAction {
* @return the sacrificed Card in its new location, or {@code null} if the
* sacrifice wasn't successful.
*/
protected final Card sacrificeDestroy(final Card c, SpellAbility cause, CardZoneTable table) {
protected final Card sacrificeDestroy(final Card c, SpellAbility cause, CardZoneTable table, Map<AbilityKey, Object> params) {
if (!c.isInPlay()) {
return null;
}
final Card newCard = moveToGraveyard(c, cause, null);
final Card newCard = moveToGraveyard(c, cause, params);
if (table != null && newCard != null && newCard.getZone() != null) {
table.put(ZoneType.Battlefield, newCard.getZone().getZoneType(), newCard);
}
@@ -2168,4 +2199,14 @@ public class GameAction {
counterTable.triggerCountersPutAll(game);
counterTable.clear();
}
public void completeDungeon(Player player, Card dungeon) {
player.addCompletedDungeon(dungeon);
ceaseToExist(dungeon, true);
// Run RoomEntered trigger
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(dungeon);
runParams.put(AbilityKey.Player, player);
game.getTriggerHandler().runTrigger(TriggerType.DungeonCompleted, runParams, false);
}
}

View File

@@ -638,6 +638,9 @@ public final class GameActionUtil {
}
public static CardCollectionView orderCardsByTheirOwners(Game game, CardCollectionView list, ZoneType dest, SpellAbility sa) {
if (list.size() <= 1) {
return list;
}
CardCollection completeList = new CardCollection();
for (Player p : game.getPlayers()) {
CardCollection subList = new CardCollection();

View File

@@ -205,7 +205,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
public boolean canBeAttached(final Card attach) {
return canBeAttached(attach, false);
}
public boolean canBeAttached(final Card attach, boolean checkSBA) {
// master mode
if (!attach.isAttachment() || attach.isCreature() || equals(attach)) {
@@ -250,7 +249,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
}
protected boolean canBeEnchantedBy(final Card aura) {
SpellAbility sa = aura.getFirstAttachSpell();
TargetRestrictions tgt = null;
if (sa != null) {
@@ -263,7 +261,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
public boolean hasProtectionFrom(final Card source) {
return hasProtectionFrom(source, false);
}
public abstract boolean hasProtectionFrom(final Card source, final boolean checkSBA);
// Counters!
@@ -280,7 +277,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
Integer value = counters.get(counterName);
return value == null ? 0 : value;
}
public final int getCounters(final CounterEnumType counterType) {
return getCounters(CounterType.get(counterType));
}
@@ -292,7 +288,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
counters.put(counterType, num);
}
}
public void setCounters(final CounterEnumType counterType, final Integer num) {
setCounters(CounterType.get(counterType), num);
}
@@ -300,7 +295,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
abstract public void setCounters(final Map<CounterType, Integer> allCounters);
abstract public boolean canReceiveCounters(final CounterType type);
abstract public int addCounter(final CounterType counterType, final int n, final Player source, final boolean applyMultiplier, final boolean fireEvents, GameEntityCounterTable table);
abstract public int addCounter(final CounterType counterType, final int n, final Player source, final SpellAbility cause, final boolean applyMultiplier, final boolean fireEvents, GameEntityCounterTable table);
abstract public void subtractCounter(final CounterType counterName, final int n);
abstract public void clearCounters();
@@ -308,8 +303,8 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
return canReceiveCounters(CounterType.get(type));
}
public int addCounter(final CounterEnumType counterType, final int n, final Player source, final boolean applyMultiplier, final boolean fireEvents, GameEntityCounterTable table) {
return addCounter(CounterType.get(counterType), n, source, applyMultiplier, fireEvents, table);
public int addCounter(final CounterEnumType counterType, final int n, final Player source, final SpellAbility cause, final boolean applyMultiplier, final boolean fireEvents, GameEntityCounterTable table) {
return addCounter(CounterType.get(counterType), n, source, cause, applyMultiplier, fireEvents, table);
}
public void subtractCounter(final CounterEnumType counterName, final int n) {
subtractCounter(CounterType.get(counterName), n);

View File

@@ -57,8 +57,28 @@ import forge.util.storage.StorageReaderRecursiveFolderWithUserFolder;
public class GameFormat implements Comparable<GameFormat> {
private final String name;
public enum FormatType {Sanctioned, Casual, Historic, Digital, Custom}
public enum FormatSubType {Block, Standard, Extended, Pioneer, Modern, Legacy, Vintage, Commander, Planechase, Videogame, MTGO, Arena, Custom}
public enum FormatType {
SANCTIONED,
CASUAL,
HISTORIC,
DIGITAL,
CUSTOM
}
public enum FormatSubType {
BLOCK,
STANDARD,
EXTENDED,
PIONEER,
MODERN,
LEGACY,
VINTAGE,
COMMANDER,
PLANECHASE,
VIDEOGAME,
MTGO,
ARENA,
CUSTOM
}
// contains allowed sets, when empty allows all sets
private FormatType formatType;
@@ -85,11 +105,11 @@ public class GameFormat implements Comparable<GameFormat> {
private final int index;
public GameFormat(final String fName, final Iterable<String> sets, final List<String> bannedCards) {
this(fName, parseDate(DEFAULTDATE), sets, bannedCards, null, false, null, null, 0, FormatType.Custom, FormatSubType.Custom);
this(fName, parseDate(DEFAULTDATE), sets, bannedCards, null, false, null, null, 0, FormatType.CUSTOM, FormatSubType.CUSTOM);
}
public static final GameFormat NoFormat = new GameFormat("(none)", parseDate(DEFAULTDATE) , null, null, null, false
, null, null, Integer.MAX_VALUE, FormatType.Custom, FormatSubType.Custom);
, null, null, Integer.MAX_VALUE, FormatType.CUSTOM, FormatSubType.CUSTOM);
public GameFormat(final String fName, final Date effectiveDate, final Iterable<String> sets, final List<String> bannedCards,
final List<String> restrictedCards, Boolean restrictedLegendary, final List<String> additionalCards,
@@ -276,6 +296,7 @@ public class GameFormat implements Comparable<GameFormat> {
if (null == other) {
return 1;
}
if (other.formatType != formatType){
return formatType.compareTo(other.formatType);
}else{
@@ -283,10 +304,11 @@ public class GameFormat implements Comparable<GameFormat> {
return formatSubType.compareTo(other.formatSubType);
}
}
if (formatType.equals(FormatType.Historic)){
if(effectiveDate!=other.effectiveDate) {//for matching dates or default dates default to name sorting
return other.effectiveDate.compareTo(effectiveDate);
}
if (formatType.equals(FormatType.HISTORIC)){
int compareDates = this.effectiveDate.compareTo(other.effectiveDate);
if (compareDates != 0)
return compareDates;
return (this.index - other.index);
}
return name.compareTo(other.name);
//return index - other.index;
@@ -338,15 +360,15 @@ public class GameFormat implements Comparable<GameFormat> {
String title = section.get("name");
FormatType formatType;
try {
formatType = FormatType.valueOf(section.get("type"));
formatType = FormatType.valueOf(section.get("type").toUpperCase());
} catch (Exception e) {
formatType = FormatType.Custom;
formatType = FormatType.CUSTOM;
}
FormatSubType formatsubType;
try {
formatsubType = FormatSubType.valueOf(section.get("subtype"));
formatsubType = FormatSubType.valueOf(section.get("subtype").toUpperCase());
} catch (Exception e) {
formatsubType = FormatSubType.Custom;
formatsubType = FormatSubType.CUSTOM;
}
Integer idx = section.getInt("order");
String dateStr = section.get("effective");
@@ -432,7 +454,7 @@ public class GameFormat implements Comparable<GameFormat> {
public Iterable<GameFormat> getSanctionedList() {
List<GameFormat> coreList = new ArrayList<>();
for(GameFormat format: naturallyOrdered){
if(format.getFormatType().equals(FormatType.Sanctioned)){
if(format.getFormatType().equals(FormatType.SANCTIONED)){
coreList.add(format);
}
}
@@ -442,8 +464,8 @@ public class GameFormat implements Comparable<GameFormat> {
public Iterable<GameFormat> getFilterList() {
List<GameFormat> coreList = new ArrayList<>();
for(GameFormat format: naturallyOrdered){
if(!format.getFormatType().equals(FormatType.Historic)
&&!format.getFormatType().equals(FormatType.Digital)){
if(!format.getFormatType().equals(FormatType.HISTORIC)
&&!format.getFormatType().equals(FormatType.DIGITAL)){
coreList.add(format);
}
}
@@ -453,7 +475,7 @@ public class GameFormat implements Comparable<GameFormat> {
public Iterable<GameFormat> getHistoricList() {
List<GameFormat> coreList = new ArrayList<>();
for(GameFormat format: naturallyOrdered){
if(format.getFormatType().equals(FormatType.Historic)){
if(format.getFormatType().equals(FormatType.HISTORIC)){
coreList.add(format);
}
}
@@ -463,7 +485,7 @@ public class GameFormat implements Comparable<GameFormat> {
public Map<String, List<GameFormat>> getHistoricMap() {
Map<String, List<GameFormat>> coreList = new HashMap<>();
for(GameFormat format: naturallyOrdered){
if(format.getFormatType().equals(FormatType.Historic)){
if(format.getFormatType().equals(FormatType.HISTORIC)){
String alpha = format.getName().substring(0,1);
if(!coreList.containsKey(alpha)){
coreList.put(alpha,new ArrayList<>());
@@ -528,15 +550,15 @@ public class GameFormat implements Comparable<GameFormat> {
Set<FormatSubType> coveredTypes = new HashSet<>();
CardPool allCards = deck.getAllCardsInASinglePool();
for(GameFormat gf : reverseDateOrdered) {
if (gf.getFormatType().equals(FormatType.Digital) && !exhaustive){
if (gf.getFormatType().equals(FormatType.DIGITAL) && !exhaustive){
//exclude Digital formats from lists for now
continue;
}
if (gf.getFormatSubType().equals(FormatSubType.Commander)){
if (gf.getFormatSubType().equals(FormatSubType.COMMANDER)){
//exclude Commander format as other deck checks are not performed here
continue;
}
if (gf.getFormatType().equals(FormatType.Historic) && coveredTypes.contains(gf.getFormatSubType())
if (gf.getFormatType().equals(FormatType.HISTORIC) && coveredTypes.contains(gf.getFormatSubType())
&& !exhaustive){
//exclude duplicate formats - only keep first of e.g. Standard historical
continue;
@@ -570,7 +592,7 @@ public class GameFormat implements Comparable<GameFormat> {
return gf1.formatSubType.compareTo(gf2.formatSubType);
}
}
if (gf1.formatType.equals(FormatType.Historic)){
if (gf1.formatType.equals(FormatType.HISTORIC)){
if(gf1.effectiveDate!=gf2.effectiveDate) {//for matching dates or default dates default to name sorting
return gf1.effectiveDate.compareTo(gf2.effectiveDate);
}

View File

@@ -28,6 +28,7 @@ import forge.card.CardStateName;
import forge.game.CardTraitBase;
import forge.game.IHasSVars;
import forge.game.ability.effects.CharmEffect;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.Card;
import forge.game.card.CardState;
import forge.game.cost.Cost;
@@ -291,9 +292,12 @@ public final class AbilityFactory {
}
if (api == ApiType.RollDice) {
for (String param : mapParams.keySet()) {
if (param.startsWith("On") || param.equals("Else")) {
spellAbility.setAdditionalAbility(param, getSubAbility(state, mapParams.get(param), sVarHolder));
final String key = "ResultSubAbilities";
if (mapParams.containsKey(key)) {
String [] diceAbilities = mapParams.get(key).split(",");
for (String ab : diceAbilities) {
String [] kv = ab.split(":");
spellAbility.setAdditionalAbility(kv[0], getSubAbility(state, kv[1], sVarHolder));
}
}
}
@@ -311,7 +315,6 @@ public final class AbilityFactory {
}
sb.append(mapParams.get("SpellDescription"));
spellAbility.setDescription(sb.toString());
} else if (api == ApiType.Charm) {
spellAbility.setDescription(CharmEffect.makeFormatedDescription(spellAbility));
@@ -319,6 +322,12 @@ public final class AbilityFactory {
spellAbility.setDescription("");
}
if (api == ApiType.RollDice) {
spellAbility.setDescription(spellAbility.getDescription() + RollDiceEffect.makeFormatedDescription(spellAbility));
} else if (api == ApiType.Repeat) {
spellAbility.setDescription(spellAbility.getDescription() + spellAbility.getAdditionalAbility("RepeatSubAbility").getDescription());
}
initializeParams(spellAbility);
makeRestrictions(spellAbility);
makeConditions(spellAbility);

View File

@@ -73,6 +73,7 @@ public enum AbilityKey {
IsCombatDamage("IsCombatDamage"),
IndividualCostPaymentInstance("IndividualCostPaymentInstance"),
IsMadness("IsMadness"),
LastStateBattlefield("LastStateBattlefield"),
LifeAmount("LifeAmount"), //TODO confirm that this and LifeGained can be merged
LifeGained("LifeGained"),
Mana("Mana"),
@@ -106,6 +107,7 @@ public enum AbilityKey {
ReplacementResult("ReplacementResult"),
ReplacementResultMap("ReplacementResultMap"),
Result("Result"),
RoomName("RoomName"),
Scheme("Scheme"),
Source("Source"),
Sources("Sources"),

View File

@@ -173,15 +173,13 @@ public class AbilityUtils {
}
}
else if (defined.equals("Targeted") && sa instanceof SpellAbility) {
final SpellAbility saTargeting = ((SpellAbility)sa).getSATargetingCard();
if (saTargeting != null) {
Iterables.addAll(cards, saTargeting.getTargets().getTargetCards());
for (TargetChoices tc : ((SpellAbility)sa).getAllTargetChoices()) {
Iterables.addAll(cards, tc.getTargetCards());
}
}
else if (defined.equals("TargetedSource") && sa instanceof SpellAbility) {
final SpellAbility saTargeting = ((SpellAbility)sa).getSATargetingSA();
if (saTargeting != null) {
for (SpellAbility s : saTargeting.getTargets().getTargetSpells()) {
for (TargetChoices tc : ((SpellAbility)sa).getAllTargetChoices()) {
for (SpellAbility s : tc.getTargetSpells()) {
cards.add(s.getHostCard());
}
}
@@ -742,7 +740,18 @@ public class AbilityUtils {
final SpellAbility root = sa.getRootAbility();
final String[] l = calcX[1].split("/");
final String m = CardFactoryUtil.extractOperators(calcX[1]);
final Integer count = (Integer) root.getTriggeringObject(AbilityKey.fromString(l[0]));
Integer count = null;
if (calcX[0].endsWith("Max")) {
@SuppressWarnings("unchecked")
Iterable<Integer> numbers = (Iterable<Integer>) root.getTriggeringObject(AbilityKey.fromString(l[0]));
for (Integer n : numbers) {
if (count == null || n > count) {
count = n;
}
}
} else {
count = (Integer) root.getTriggeringObject(AbilityKey.fromString(l[0]));
}
val = doXMath(ObjectUtils.firstNonNull(count, 0), m, card, ability);
}
@@ -807,9 +816,9 @@ public class AbilityUtils {
final SpellAbility root = sa.getRootAbility();
list = new CardCollection((Card) root.getReplacingObject(AbilityKey.fromString(calcX[0].substring(8))));
}
if (list != null) {
// there could be null inside!
list = Iterables.filter(list, Card.class);
if (list != null) {
val = handlePaid(list, calcX[1], card, ability);
}
}
@@ -1008,9 +1017,8 @@ public class AbilityUtils {
players.addAll(getDefinedPlayers(card, "TargetedController", sa));
}
else if ((defined.equals("Targeted") || defined.equals("TargetedPlayer")) && sa instanceof SpellAbility) {
final SpellAbility saTargeting = ((SpellAbility)sa).getSATargetingPlayer();
if (saTargeting != null) {
players.addAll(saTargeting.getTargets().getTargetPlayers());
for (TargetChoices tc : ((SpellAbility)sa).getAllTargetChoices()) {
players.addAll(tc.getTargetPlayers());
}
}
else if (defined.equals("ParentTarget") && sa instanceof SpellAbility) {
@@ -1289,8 +1297,7 @@ public class AbilityUtils {
SpellAbility s = null;
// TODO - this probably needs to be fleshed out a bit, but the basics
// work
// TODO - this probably needs to be fleshed out a bit, but the basics work
if (defined.equals("Self") && sa instanceof SpellAbility) {
s = (SpellAbility)sa;
}
@@ -1298,9 +1305,8 @@ public class AbilityUtils {
s = ((SpellAbility)sa).getRootAbility();
}
else if (defined.equals("Targeted") && sa instanceof SpellAbility) {
final SpellAbility saTargeting = ((SpellAbility)sa).getSATargetingSA();
if (saTargeting != null) {
for (SpellAbility targetSpell : saTargeting.getTargets().getTargetSpells()) {
for (TargetChoices tc : ((SpellAbility)sa).getAllTargetChoices()) {
for (SpellAbility targetSpell : tc.getTargetSpells()) {
SpellAbilityStackInstance stackInstance = game.getStack().getInstanceFromSpellAbility(targetSpell);
if (stackInstance != null) {
SpellAbility instanceSA = stackInstance.getSpellAbility(true);
@@ -1412,7 +1418,6 @@ public class AbilityUtils {
}
return;
}
AbilityUtils.resolveApiAbility(sa, game);
}
@@ -2575,7 +2580,6 @@ public class AbilityUtils {
// Count$ThisTurnCast <Valid>
// Count$LastTurnCast <Valid>
if (sq[0].startsWith("ThisTurnCast") || sq[0].startsWith("LastTurnCast")) {
final String[] workingCopy = l[0].split("_");
final String validFilter = workingCopy[1];
@@ -2736,7 +2740,6 @@ public class AbilityUtils {
return doXMath(powers.size(), expr, c, ctb);
}
if (sq[0].startsWith("MostProminentCreatureType")) {
String restriction = l[0].split(" ")[1];
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
@@ -3366,6 +3369,29 @@ public class AbilityUtils {
return doXMath(opps == null ? 0 : opps.size(), m, source, ctb);
}
if (value.equals("DungeonsCompleted")) {
return doXMath(player.getCompletedDungeons().size(), m, source, ctb);
}
if (value.equals("DifferentlyNamedDungeonsCompleted")) {
int amount = 0;
List<Card> dungeons = player.getCompletedDungeons();
for (int i = 0; i < dungeons.size(); ++i) {
Card d1 = dungeons.get(i);
boolean hasSameName = false;
for (int j = i - 1; j >= 0; --j) {
Card d2 = dungeons.get(j);
if (d1.getName().equals(d2.getName())) {
hasSameName = true;
break;
}
}
if (!hasSameName) {
++amount;
}
}
return doXMath(amount, m, source, ctb);
}
return doXMath(0, m, source, ctb);
}
@@ -3395,7 +3421,6 @@ public class AbilityUtils {
return doXMath(n, CardFactoryUtil.extractOperators(s), source, ctb);
}
/**
* <p>
* handlePaid.

View File

@@ -172,6 +172,7 @@ public enum ApiType {
UnattachAll (UnattachAllEffect.class),
Untap (UntapEffect.class),
UntapAll (UntapAllEffect.class),
Venture (VentureEffect.class),
Vote (VoteEffect.class),
WinsGame (GameWinEffect.class),

View File

@@ -329,7 +329,6 @@ public abstract class SpellAbilityEffect {
}
protected static void addSelfTrigger(final SpellAbility sa, String location, final Card card) {
String trigStr = "Mode$ Phase | Phase$ End of Turn | TriggerZones$ Battlefield " +
"| TriggerDescription$ At the beginning of the end step, " + location.toLowerCase() + " CARDNAME.";
@@ -511,7 +510,7 @@ public abstract class SpellAbilityEffect {
}
}
// build an Effect with that infomation
// build an Effect with that information
String name = host.getName() + "'s Effect";
final Card eff = createEffect(sa, controller, name, host.getImageKey());
@@ -617,8 +616,29 @@ public abstract class SpellAbilityEffect {
combat.addBlocker(attacker, c);
combat.orderAttackersForDamageAssignment(c);
{
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Attacker, attacker);
runParams.put(AbilityKey.Blocker, c);
game.getTriggerHandler().runTrigger(TriggerType.AttackerBlockedByCreature, runParams, false);
}
{
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Attackers, attacker);
game.getTriggerHandler().runTrigger(TriggerType.AttackerBlockedOnce, runParams, false);
}
// Run triggers for new blocker and add it to damage assignment order
if (!wasBlocked) {
final CardCollection blockers = combat.getBlockers(attacker);
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Attacker, attacker);
runParams.put(AbilityKey.Blockers, blockers);
runParams.put(AbilityKey.NumBlockers, blockers.size());
runParams.put(AbilityKey.Defender, combat.getDefenderByAttacker(attacker));
runParams.put(AbilityKey.DefendingPlayer, combat.getDefenderPlayerByAttacker(attacker));
game.getTriggerHandler().runTrigger(TriggerType.AttackerBlocked, runParams, false);
combat.setBlocked(attacker, true);
combat.addBlockerToDamageAssignmentOrder(attacker, c);
}

View File

@@ -93,7 +93,7 @@ public class AmassEffect extends TokenEffectBase {
GameEntityCounterTable table = new GameEntityCounterTable();
for(final Card tgtCard : tgtCards) {
tgtCard.addCounter(CounterEnumType.P1P1, amount, activator, true, table);
tgtCard.addCounter(CounterEnumType.P1P1, amount, activator, sa, true, table);
game.updateLastStateForCard(tgtCard);
if (remember) {

View File

@@ -59,6 +59,9 @@ public class AnimateAllEffect extends AnimateEffectBase {
if (types.hasSubtype("ChosenType")) {
types.clear();
types.add(host.getChosenType());
} else if (types.hasSubtype("ChosenType2")) {
types.clear();
types.add(host.getChosenType2());
}
final List<String> keywords = new ArrayList<>();

View File

@@ -71,6 +71,9 @@ public class AnimateEffect extends AnimateEffectBase {
if (types.hasSubtype("ChosenType")) {
types.clear();
types.add(source.getChosenType());
} else if (types.hasSubtype("ChosenType2")) {
types.clear();
types.add(source.getChosenType2());
}
final List<String> keywords = Lists.newArrayList();

View File

@@ -120,7 +120,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
if (!addType.isEmpty() || !removeType.isEmpty() || removeCreatureTypes) {
c.addChangedCardTypes(addType, removeType, removeSuperTypes, removeCardTypes, removeSubTypes,
removeLandTypes, removeCreatureTypes, removeArtifactTypes, removeEnchantmentTypes, timestamp);
removeLandTypes, removeCreatureTypes, removeArtifactTypes, removeEnchantmentTypes, timestamp, true, false);
}
c.addChangedCardKeywords(keywords, removeKeywords, removeAll, removeLandTypes, timestamp);
@@ -133,7 +133,7 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
c.addHiddenExtrinsicKeyword(k);
}
c.addColor(colors, !sa.hasParam("OverwriteColors"), timestamp);
c.addColor(colors, !sa.hasParam("OverwriteColors"), timestamp, false);
if (sa.hasParam("LeaveBattlefield")) {
addLeaveBattlefieldReplacement(c, sa, sa.getParam("LeaveBattlefield"));

View File

@@ -118,7 +118,6 @@ public class AttachEffect extends SpellAbilityEffect {
* the o
*/
public static void handleAttachment(final Card card, final Object o, final SpellAbility sa) {
if (card == null) { return; }
if (o instanceof Card) {

View File

@@ -7,6 +7,7 @@ import java.util.Map;
import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -47,6 +48,7 @@ public class BalanceEffect extends SpellAbilityEffect {
min = Math.min(min, validCards.get(i).size());
}
Map<AbilityKey, Object> params = AbilityKey.newMap();
CardZoneTable table = new CardZoneTable();
for (int i = 0; i < players.size(); i++) {
Player p = players.get(i);
@@ -59,7 +61,7 @@ public class BalanceEffect extends SpellAbilityEffect {
} else { // Battlefield
for (Card card : p.getController().choosePermanentsToSacrifice(sa, numToBalance, numToBalance, validCards.get(i), valid)) {
if ( null == card ) continue;
game.getAction().sacrifice(card, sa, table);
game.getAction().sacrifice(card, sa, table, params);
}
}
}

View File

@@ -291,7 +291,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
* @return a {@link java.lang.String} object.
*/
private static String changeKnownOriginStackDescription(final SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
final Card host = sa.getHostCard();
@@ -709,7 +708,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
if (sa.hasParam("WithCountersType")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(hostCard, sa.getParamOrDefault("WithCountersAmount", "1"), sa);
movedCard.addCounter(cType, cAmount, player, true, counterTable);
movedCard.addCounter(cType, cAmount, player, sa, true, counterTable);
}
if (sa.hasParam("ExileFaceDown") || sa.hasParam("FaceDown")) {

View File

@@ -200,7 +200,6 @@ public class CharmEffect extends SpellAbilityEffect {
}
private static void chainAbilities(SpellAbility sa, List<AbilitySub> chosen) {
if (chosen == null) {
return;
}
@@ -243,5 +242,4 @@ public class CharmEffect extends SpellAbilityEffect {
}
}

View File

@@ -65,7 +65,7 @@ public class ChooseTypeEffect extends SpellAbilityEffect {
for (final Player p : tgtPlayers) {
if ((tgt == null) || p.canBeTargetedBy(sa)) {
String choice = p.getController().chooseSomeType(type, sa, validTypes, invalidTypes);
if (!sa.hasParam("Secondary")) {
if (!sa.hasParam("ChooseType2")) {
card.setChosenType(choice);
} else {
card.setChosenType2(choice);

View File

@@ -143,7 +143,7 @@ public class CloneEffect extends SpellAbilityEffect {
tgtCard.updateStateForView();
// when clone is itself, cleanup from old abilities
if (host.equals(tgtCard)) {
if (host.equals(tgtCard) && !sa.hasParam("ImprintRememberedNoCleanup")) {
tgtCard.clearImprintedCards();
tgtCard.clearRemembered();
}

View File

@@ -4,6 +4,7 @@ import java.util.List;
import forge.GameCommand;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -28,6 +29,8 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) {
final Player activator = sa.getActivatingPlayer();
final Game game = activator.getGame();
final Player controller = sa.hasParam("Controller") ? AbilityUtils.getDefinedPlayers(
sa.getHostCard(), sa.getParam("Controller"), sa).get(0) : activator;
List<Player> tgtPlayers = getTargetPlayers(sa);
@@ -37,12 +40,12 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
@Override
public void run() {
// CR 800.4b
if (activator.hasLost()) {
if (controller.hasLost()) {
return;
}
long ts = game.getNextTimestamp();
pTarget.addController(ts, activator);
pTarget.addController(ts, controller);
// on following cleanup release control
game.getEndOfTurn().addUntil(new GameCommand() {

View File

@@ -99,7 +99,7 @@ public class CopyPermanentEffect extends TokenEffectBase {
if (sa.hasParam("RandomCopied")) {
List<PaperCard> copysource = Lists.newArrayList(cards);
List<Card> choice = Lists.newArrayList();
final String num = sa.hasParam("RandomNum") ? sa.getParam("RandomNum") : "1";
final String num = sa.getParamOrDefault("RandomNum","1");
int ncopied = AbilityUtils.calculateAmount(host, num, sa);
while (ncopied > 0 && !copysource.isEmpty()) {
final PaperCard cp = Aggregates.random(copysource);

View File

@@ -73,10 +73,8 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
controllers = AbilityUtils.getDefinedPlayers(card, sa.getParam("Controller"), sa);
}
final List<SpellAbility> tgtSpells = getTargetSpells(sa);
if (tgtSpells.size() == 0 || amount == 0) {
return;
}

View File

@@ -12,6 +12,7 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardZoneTable;
import forge.game.replacement.ReplacementResult;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
@@ -113,6 +114,8 @@ public class CounterEffect extends SpellAbilityEffect {
}
}
Map<AbilityKey, Object> params = AbilityKey.newMap();
CardZoneTable table = new CardZoneTable();
for (final SpellAbility tgtSA : sas) {
final Card tgtSACard = tgtSA.getHostCard();
// should remember even that spell cannot be countered, e.g. Dovescape
@@ -137,7 +140,7 @@ public class CounterEffect extends SpellAbilityEffect {
// Destroy Permanent may be able to be turned into a SubAbility
if (tgtSA.isAbility() && sa.hasParam("DestroyPermanent")) {
game.getAction().destroy(tgtSACard, sa, true, null);
game.getAction().destroy(tgtSACard, sa, true, table, params);
}
if (sa.hasParam("RememberCountered")) {
@@ -152,6 +155,7 @@ public class CounterEffect extends SpellAbilityEffect {
}
}
}
table.triggerChangesZoneAll(game, sa);
} // end counterResolve
/**
@@ -207,7 +211,6 @@ public class CounterEffect extends SpellAbilityEffect {
} else if (destination.equals("Battlefield")) {
if (tgtSA instanceof SpellPermanent) {
Card c = tgtSA.getHostCard();
System.out.println(c + " is SpellPermanent");
c.setController(srcSA.getActivatingPlayer(), 0);
game.getAction().moveToPlay(c, srcSA.getActivatingPlayer(), srcSA, params);
} else {

View File

@@ -147,8 +147,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
Map<CounterType, Integer> countersToAdd = Maps.newHashMap();
for (Card src : srcCards) {
// rule 121.5: If the first and second objects are the same object, nothing
// happens
// rule 121.5: If the first and second objects are the same object, nothing happens
if (src.equals(dest)) {
continue;
}
@@ -163,7 +162,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
}
}
for (Map.Entry<CounterType, Integer> e : countersToAdd.entrySet()) {
dest.addCounter(e.getKey(), e.getValue(), player, true, table);
dest.addCounter(e.getKey(), e.getValue(), player, sa, true, table);
}
game.updateLastStateForCard(dest);
@@ -199,8 +198,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
boolean updateSource = false;
for (final Card dest : tgtCards) {
// rule 121.5: If the first and second objects are the same object, nothing
// happens
// rule 121.5: If the first and second objects are the same object, nothing happens
if (source.equals(dest)) {
continue;
}
@@ -225,7 +223,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
if (cnum > 0) {
source.subtractCounter(cType, cnum);
cur.addCounter(cType, cnum, player, true, table);
cur.addCounter(cType, cnum, player, sa, true, table);
game.updateLastStateForCard(cur);
updateSource = true;
}
@@ -262,8 +260,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
for (final Card dest : tgtCards) {
if (null != dest) {
// rule 121.5: If the first and second objects are the same object, nothing
// happens
// rule 121.5: If the first and second objects are the same object, nothing happens
if (source.equals(dest)) {
continue;
}
@@ -311,7 +308,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
}
for (Map.Entry<CounterType, Integer> e : countersToAdd.entrySet()) {
cur.addCounter(e.getKey(), e.getValue(), player, true, table);
cur.addCounter(e.getKey(), e.getValue(), player, sa, true, table);
}
game.updateLastStateForCard(cur);
}
@@ -329,8 +326,7 @@ public class CountersMoveEffect extends SpellAbilityEffect {
final PlayerController pc = player.getController();
final Game game = host.getGame();
// rule 121.5: If the first and second objects are the same object, nothing
// happens
// rule 121.5: If the first and second objects are the same object, nothing happens
if (src.equals(dest)) {
return;
}

View File

@@ -54,10 +54,10 @@ public class CountersMultiplyEffect extends SpellAbilityEffect {
continue;
}
if (counterType != null) {
gameCard.addCounter(counterType, gameCard.getCounters(counterType) * n, player, true, table);
gameCard.addCounter(counterType, gameCard.getCounters(counterType) * n, player, sa, true, table);
} else {
for (Map.Entry<CounterType, Integer> e : gameCard.getCounters().entrySet()) {
gameCard.addCounter(e.getKey(), e.getValue() * n, player, true, table);
gameCard.addCounter(e.getKey(), e.getValue() * n, player, sa, true, table);
}
}
game.updateLastStateForCard(gameCard);

View File

@@ -30,7 +30,7 @@ public class CountersNoteEffect extends SpellAbilityEffect {
if (mode.equals(MODE_STORE)) {
noteCounters(c, source);
} else if (mode.equals(MODE_LOAD)) {
loadCounters(c, source, p, table);
loadCounters(c, source, p, sa, table);
}
}
table.triggerCountersPutAll(game);
@@ -44,13 +44,13 @@ public class CountersNoteEffect extends SpellAbilityEffect {
}
}
private void loadCounters(Card notee, Card source, final Player p, GameEntityCounterTable table) {
private void loadCounters(Card notee, Card source, final Player p, final SpellAbility sa, GameEntityCounterTable table) {
for(Entry<String, String> svar : source.getSVars().entrySet()) {
String key = svar.getKey();
if (key.startsWith(NOTE_COUNTERS)) {
notee.addCounter(
CounterType.getType(key.substring(NOTE_COUNTERS.length())),
Integer.parseInt(svar.getValue()), p, false, table);
Integer.parseInt(svar.getValue()), p, sa, false, table);
}
// TODO Probably should "remove" the svars that were temporarily used
}

View File

@@ -48,7 +48,7 @@ public class CountersProliferateEffect extends SpellAbilityEffect {
GameEntityCounterTable table = new GameEntityCounterTable();
for (final GameEntity ge : result) {
for (final CounterType ct : ge.getCounters().keySet()) {
ge.addCounter(ct, 1, p, true, true, table);
ge.addCounter(ct, 1, p, sa, true, true, table);
}
if (ge instanceof Card) {
Card c = (Card) ge;

View File

@@ -71,7 +71,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect {
if (etbcounter) {
tgtCard.addEtbCounter(CounterType.getType(type), counterAmount, placer);
} else {
tgtCard.addCounter(CounterType.getType(type), counterAmount, placer, inBattlefield, table);
tgtCard.addCounter(CounterType.getType(type), counterAmount, placer, sa, inBattlefield, table);
}
game.updateLastStateForCard(tgtCard);
}

View File

@@ -226,10 +226,10 @@ public class CountersPutEffect extends SpellAbilityEffect {
if (eachExistingCounter) {
for (CounterType ct : choices) {
if (obj instanceof Player) {
((Player) obj).addCounter(ct, counterAmount, placer, true, table);
((Player) obj).addCounter(ct, counterAmount, placer, sa, true, table);
}
if (obj instanceof Card) {
gameCard.addCounter(ct, counterAmount, placer, true, table);
gameCard.addCounter(ct, counterAmount, placer, sa, true, table);
}
}
continue;
@@ -253,7 +253,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
for (Card c : AbilityUtils.getDefinedCards(card, sa.getParam("EachFromSource"), sa)) {
for (Entry<CounterType, Integer> cti : c.getCounters().entrySet()) {
if (gameCard != null && gameCard.canReceiveCounters(cti.getKey())) {
gameCard.addCounter(cti.getKey(), cti.getValue(), placer, true, table);
gameCard.addCounter(cti.getKey(), cti.getValue(), placer, sa, true, table);
}
}
}
@@ -338,7 +338,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
if (etbcounter) {
gameCard.addEtbCounter(counterType, counterAmount, placer);
} else {
int addedAmount = gameCard.addCounter(counterType, counterAmount, placer, true, table);
int addedAmount = gameCard.addCounter(counterType, counterAmount, placer, sa, true, table);
if (addedAmount > 0) {
counterAdded = true;
}
@@ -372,7 +372,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
if (etbcounter) {
gameCard.addEtbCounter(counterType, counterAmount, placer);
} else {
if (gameCard.addCounter(counterType, counterAmount, placer, false, table) > 0) {
if (gameCard.addCounter(counterType, counterAmount, placer, sa, false, table) > 0) {
counterAdded = true;
}
}
@@ -388,7 +388,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
} else if (obj instanceof Player) {
// Add Counters to players!
Player pl = (Player) obj;
pl.addCounter(counterType, counterAmount, placer, true, table);
pl.addCounter(counterType, counterAmount, placer, sa, true, table);
}
}
}

View File

@@ -111,7 +111,7 @@ public class CountersPutOrRemoveEffect extends SpellAbilityEffect {
boolean apply = zone == null || zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Stack);
tgtCard.addCounter(chosenType, counterAmount, pl, apply, table);
tgtCard.addCounter(chosenType, counterAmount, pl, sa, apply, table);
} else {
tgtCard.subtractCounter(chosenType, counterAmount);
}

View File

@@ -31,7 +31,6 @@ public class DelayedTriggerEffect extends SpellAbilityEffect {
}
return "";
}
@Override

View File

@@ -6,6 +6,7 @@ import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -44,7 +45,6 @@ public class DestroyAllEffect extends SpellAbilityEffect {
*/
@Override
public void resolve(SpellAbility sa) {
final boolean noRegen = sa.hasParam("NoRegen");
final Card card = sa.getHostCard();
final Game game = sa.getActivatingPlayer().getGame();
@@ -90,10 +90,12 @@ public class DestroyAllEffect extends SpellAbilityEffect {
}
CardZoneTable table = new CardZoneTable();
Map<AbilityKey, Object> params = AbilityKey.newMap();
params.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield());
Map<Integer, Card> cachedMap = Maps.newHashMap();
for (Card c : list) {
if (game.getAction().destroy(c, sa, !noRegen, table) && remDestroyed) {
if (game.getAction().destroy(c, sa, !noRegen, table, params) && remDestroyed) {
card.addRemembered(CardUtil.getLKICopy(c, cachedMap));
}
}

View File

@@ -8,9 +8,10 @@ import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardUtil;
import forge.game.card.CardZoneTable;
import forge.game.spellability.SpellAbility;
@@ -72,13 +73,16 @@ public class DestroyEffect extends SpellAbilityEffect {
card.clearRemembered();
}
CardCollection tgtCards = getTargetCards(sa);
CardCollection untargetedCards = CardUtil.getRadiance(sa);
CardCollectionView tgtCards = getTargetCards(sa);
CardCollectionView untargetedCards = CardUtil.getRadiance(sa);
if (tgtCards.size() > 1) {
tgtCards = (CardCollection) GameActionUtil.orderCardsByTheirOwners(game, tgtCards, ZoneType.Graveyard, sa);
tgtCards = GameActionUtil.orderCardsByTheirOwners(game, tgtCards, ZoneType.Graveyard, sa);
}
Map<AbilityKey, Object> params = AbilityKey.newMap();
params.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield());
CardZoneTable table = new CardZoneTable();
Map<Integer, Card> cachedMap = Maps.newHashMap();
for (final Card tgtC : tgtCards) {
@@ -90,24 +94,24 @@ public class DestroyEffect extends SpellAbilityEffect {
if (gameCard == null || !tgtC.equalsWithTimestamp(gameCard)) {
continue;
}
internalDestroy(gameCard, sa, table, cachedMap);
internalDestroy(gameCard, sa, table, cachedMap, params);
}
}
if (untargetedCards.size() > 1) {
untargetedCards = (CardCollection) GameActionUtil.orderCardsByTheirOwners(game, untargetedCards, ZoneType.Graveyard, sa);
untargetedCards = GameActionUtil.orderCardsByTheirOwners(game, untargetedCards, ZoneType.Graveyard, sa);
}
for (final Card unTgtC : untargetedCards) {
if (unTgtC.isInPlay()) {
internalDestroy(unTgtC, sa, table, cachedMap);
internalDestroy(unTgtC, sa, table, cachedMap, params);
}
}
table.triggerChangesZoneAll(game, sa);
}
protected void internalDestroy(Card gameCard, SpellAbility sa, CardZoneTable table, Map<Integer, Card> cachedMap) {
protected void internalDestroy(Card gameCard, SpellAbility sa, CardZoneTable table, Map<Integer, Card> cachedMap, Map<AbilityKey, Object> params) {
final Card card = sa.getHostCard();
final Game game = card.getGame();
@@ -122,9 +126,9 @@ public class DestroyEffect extends SpellAbilityEffect {
card.addRemembered(gameCard.getAttachedCards());
}
if (sac) {
destroyed = game.getAction().sacrifice(gameCard, sa, table) != null;
destroyed = game.getAction().sacrifice(gameCard, sa, table, params) != null;
} else {
destroyed = game.getAction().destroy(gameCard, sa, !noRegen, table);
destroyed = game.getAction().destroy(gameCard, sa, !noRegen, table, params);
}
if (destroyed && remDestroyed) {
card.addRemembered(gameCard);

View File

@@ -336,7 +336,7 @@ public class DigEffect extends SpellAbilityEffect {
} else if (destZone1.equals(ZoneType.Exile)) {
if (sa.hasParam("ExileWithCounter")) {
c.addCounter(CounterType.getType(sa.getParam("ExileWithCounter")),
1, player, true, counterTable);
1, player, sa, true, counterTable);
}
c.setExiledWith(effectHost);
c.setExiledBy(effectHost.getController());
@@ -408,7 +408,7 @@ public class DigEffect extends SpellAbilityEffect {
if (destZone2 == ZoneType.Exile) {
if (sa.hasParam("ExileWithCounter")) {
c.addCounter(CounterType.getType(sa.getParam("ExileWithCounter")),
1, player, true, counterTable);
1, player, sa, true, counterTable);
}
c.setExiledWith(effectHost);
c.setExiledBy(effectHost.getController());

View File

@@ -173,7 +173,7 @@ public class DigUntilEffect extends SpellAbilityEffect {
if (optionalFound && !p.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblDoYouWantPutCardToZone", foundDest.getTranslatedName()))) {
continue;
} else {
}
Card m = null;
if (sa.hasParam("GainControl") && foundDest.equals(ZoneType.Battlefield)) {
c.setController(sa.getActivatingPlayer(), game.getNextTimestamp());
@@ -195,7 +195,6 @@ public class DigUntilEffect extends SpellAbilityEffect {
}
}
}
}
if (sa.hasParam("RememberRevealed")) {
for (final Card c : revealed) {

View File

@@ -18,6 +18,8 @@ public class ETBReplacementEffect extends SpellAbilityEffect {
Map<AbilityKey, Object> params = AbilityKey.newMap();
params.put(AbilityKey.CardLKI, sa.getReplacingObject(AbilityKey.CardLKI));
params.put(AbilityKey.ReplacementEffect, sa.getReplacementEffect());
sa.getActivatingPlayer().getGame().getAction().moveToPlay(card, card.getController(), sa, params);
final SpellAbility root = sa.getRootAbility();
SpellAbility cause = (SpellAbility) root.getReplacingObject(AbilityKey.Cause);
sa.getActivatingPlayer().getGame().getAction().moveToPlay(card, card.getController(), cause, params);
}
}

View File

@@ -247,9 +247,12 @@ public class EffectEffect extends SpellAbilityEffect {
}
// Set Chosen Type
if (!hostCard.getChosenType().isEmpty()) {
if (hostCard.hasChosenType()) {
eff.setChosenType(hostCard.getChosenType());
}
if (hostCard.hasChosenType2()) {
eff.setChosenType2(hostCard.getChosenType2());
}
// Set Chosen name
if (!hostCard.getNamedCard().isEmpty()) {

View File

@@ -83,7 +83,7 @@ public class ExploreEffect extends SpellAbilityEffect {
// if the card is not more in the game anymore
// this might still return true but its no problem
if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(c)) {
c.addCounter(CounterEnumType.P1P1, 1, pl, true, table);
c.addCounter(CounterEnumType.P1P1, 1, pl, sa, true, table);
}
}

View File

@@ -65,7 +65,8 @@ public class FightEffect extends DamageBaseEffect {
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeFight", CardTranslation.getTranslatedName(fighters.get(0).getName()), CardTranslation.getTranslatedName(fighters.get(1).getName())))) {
return;
} else {
}
dealDamage(sa, fighters.get(0), fighters.get(1));
for (Card c : fighters) {
@@ -76,8 +77,6 @@ public class FightEffect extends DamageBaseEffect {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Fighters, fighters);
game.getTriggerHandler().runTrigger(TriggerType.FightOnce, runParams, false);
}
}
private static List<Card> getFighters(SpellAbility sa) {

View File

@@ -58,8 +58,8 @@ public class ImmediateTriggerEffect extends SpellAbilityEffect {
Card lki = CardUtil.getLKICopy(gameCard);
lki.clearControllers();
lki.setOwner(sa.getActivatingPlayer());
// if this trigger is part of ETBReplacement it shouldn't run with LKI from incomplete zone change (Wall of Stolen Identity)
final Card trigHost = sa.getRootAbility().getReplacementEffect() != null && sa.getRootAbility().getReplacementEffect().getMode().equals(ReplacementType.Moved) ? gameCard : lki;
// if this trigger is part of ETBReplacement it shouldn't run with LKI from incomplete zone change (Mimic Vat + Wall of Stolen Identity)
final Card trigHost = sa.getRootAbility().getReplacementEffect() != null && sa.getRootAbility().getReplacementEffect().getMode().equals(ReplacementType.Moved) && gameCard.getZone() == null ? gameCard : lki;
final Trigger immediateTrig = TriggerHandler.parseTrigger(mapParams, trigHost, sa.isIntrinsic(), null);
immediateTrig.setSpawningAbility(sa.copy(lki, sa.getActivatingPlayer(), true));

View File

@@ -3,6 +3,7 @@ package forge.game.ability.effects;
import static forge.util.TextUtil.toManaString;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
@@ -61,6 +62,22 @@ public class ManaEffect extends SpellAbilityEffect {
String[] colorsNeeded = express.isEmpty() ? null : express.split(" ");
boolean differentChoice = abMana.getOrigProduced().contains("Different");
ColorSet fullOptions = colorOptions;
// Use specifyManaCombo if possible
if (colorsNeeded == null && amount > 1 && !sa.hasParam("TwoEach")) {
Map<Byte, Integer> choices = p.getController().specifyManaCombo(sa, colorOptions, amount, differentChoice);
for (Map.Entry<Byte, Integer> e : choices.entrySet()) {
Byte chosenColor = e.getKey();
String choice = MagicColor.toShortString(chosenColor);
Integer count = e.getValue();
while (count > 0) {
if (choiceString.length() > 0) {
choiceString.append(" ");
}
choiceString.append(choice);
--count;
}
}
} else {
for (int nMana = 0; nMana < amount; nMana++) {
String choice = "";
if (colorsNeeded != null && colorsNeeded.length > nMana) { // select from express choices if possible
@@ -91,6 +108,7 @@ public class ManaEffect extends SpellAbilityEffect {
choiceString.append(" ").append(choice);
}
}
}
if (choiceString.toString().isEmpty() && "Combo ColorIdentity".equals(abMana.getOrigProduced())) {
// No mana could be produced here (non-EDH match?), so cut short

View File

@@ -183,18 +183,11 @@ public class PlayEffect extends SpellAbilityEffect {
activator.addController(controlledByTimeStamp, controlledByPlayer);
}
boolean singleOption = tgtCards.size() == 1 && amount == 1 && optional;
while (!tgtCards.isEmpty() && amount > 0) {
activator.getController().tempShowCards(showCards);
Card tgtCard = null;
if (tgtCards.size() == 1 && amount == 1 && optional) {
tgtCard = tgtCards.get(0);
if (!controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())))) {
break;
}
} else {
tgtCard = controller.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblSelectCardToPlay"), optional, null);
}
Card tgtCard = controller.getController().chooseSingleEntityForEffect(tgtCards, sa, Localizer.getInstance().getMessage("lblSelectCardToPlay"), !singleOption && optional, null);
activator.getController().endTempShowCards();
if (tgtCard == null) {
break;
@@ -210,12 +203,16 @@ public class PlayEffect extends SpellAbilityEffect {
game.getAction().revealTo(tgtCard, activator);
}
if (!sa.hasParam("AllowRepeats")) {
tgtCards.remove(tgtCard);
if (singleOption && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())))) {
if (wasFaceDown) {
tgtCard.turnFaceDownNoUpdate();
tgtCard.updateStateForView();
}
break;
}
if (wasFaceDown) {
tgtCard.updateStateForView();
if (!sa.hasParam("AllowRepeats")) {
tgtCards.remove(tgtCard);
}
final Card original = tgtCard;
@@ -272,6 +269,10 @@ public class PlayEffect extends SpellAbilityEffect {
}
// in case player canceled from choice dialog
if (tgtSA == null) {
if (wasFaceDown) {
tgtCard.turnFaceDownNoUpdate();
tgtCard.updateStateForView();
}
continue;
}
@@ -329,7 +330,7 @@ public class PlayEffect extends SpellAbilityEffect {
source.addRemembered(tgtSA.getHostCard());
}
//Forgot only of playing was successful
//Forgot only if playing was successful
if (sa.hasParam("ForgetRemembered")) {
source.clearRemembered();
}
@@ -397,7 +398,6 @@ public class PlayEffect extends SpellAbilityEffect {
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
}
protected void addIllusionaryMaskReplace(Card c, SpellAbility sa) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();

View File

@@ -150,7 +150,6 @@ public class PumpEffect extends SpellAbilityEffect {
*/
@Override
protected String getStackDescription(final SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
List<GameEntity> tgts = Lists.newArrayList();
tgts.addAll(getCardsfromTargets(sa));

Some files were not shown because too many files have changed in this diff Show More