diff --git a/.gitignore b/.gitignore
index 1f236e77908..1f0dde4c51f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,8 @@ bin
gen
*.log
+# Ignore macOS Spotlight rubbish
+.DS_Store
# TODO: specify what these ignores are for (releasing?)
diff --git a/README.md b/README.md
index 7e4bedb6df9..26c3d1e5e0a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[Official GitLab repo](https://git.cardforge.org/core-developers/forge).
-Dev instructions here: [Getting Started](https://www.slightlymagic.net/wiki/Forge:How_to_Get_Started_Developing_Forge) (Somewhat outdated)
+Dev instructions here: [Getting Started](https://git.cardforge.org/core-developers/forge/-/wikis/(SM-autoconverted)-how-to-get-started-developing-forge) (Somewhat outdated)
Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968)
@@ -168,11 +168,11 @@ The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desk
## IntelliJ
-Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/wikis/intellij-setup).
+Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/-/wikis/Development/intellij-setup).
## Card Scripting
-Visit [this page](https://www.slightlymagic.net/wiki/Forge_API) for information on scripting.
+Visit [this page](https://git.cardforge.org/core-developers/forge/-/wikis/Card-scripting-API/Card-scripting-API) for information on scripting.
Card scripting resources are found in the forge-gui/res/ path.
diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java
index c3ef8885a03..e0c01f01feb 100644
--- a/forge-ai/src/main/java/forge/ai/AiAttackController.java
+++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java
@@ -54,7 +54,6 @@ import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
-//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
/**
*
* ComputerUtil_Attack2 class.
@@ -412,12 +411,10 @@ public class AiAttackController {
final Player opp = this.defendingOpponent;
- // Increase the total number of blockers needed by 1 if Finest Hour in
- // play
+ // Increase the total number of blockers needed by 1 if Finest Hour in play
// (human will get an extra first attack with a creature that untaps)
// In addition, if the computer guesses it needs no blockers, make sure
- // that
- // it won't be surprised by Exalted
+ // that it won't be surprised by Exalted
final int humanExaltedBonus = opp.countExaltedBonus();
if (humanExaltedBonus > 0) {
@@ -427,8 +424,7 @@ public class AiAttackController {
// total attack = biggest creature + exalted, *2 if Rafiq is in play
int humanBasePower = getAttack(this.oppList.get(0)) + humanExaltedBonus;
if (finestHour) {
- // For Finest Hour, one creature could attack and get the
- // bonus TWICE
+ // For Finest Hour, one creature could attack and get the bonus TWICE
humanBasePower = humanBasePower + humanExaltedBonus;
}
final int totalExaltedAttack = opp.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower
@@ -449,7 +445,6 @@ public class AiAttackController {
return notNeededAsBlockers;
}
- // this uses a global variable, which isn't perfect
public final boolean doesHumanAttackAndWin(final Player ai, final int nBlockingCreatures) {
int totalAttack = 0;
int totalPoison = 0;
@@ -681,7 +676,6 @@ public class AiAttackController {
* @return a {@link forge.game.combat.Combat} object.
*/
public final void declareAttackers(final Combat combat) {
-
if (this.attackers.isEmpty()) {
return;
}
@@ -716,7 +710,7 @@ public class AiAttackController {
int attackMax = restrict.getMax();
if (attackMax == -1) {
// check with the local limitations vs. the chosen defender
- attackMax = ComputerUtilCombat.getMaxAttackersFor(defender);
+ attackMax = restrict.getDefenderMax().get(defender) == null ? -1 : restrict.getDefenderMax().get(defender);
}
if (attackMax == 0) {
@@ -776,7 +770,6 @@ public class AiAttackController {
return;
}
-
if (bAssault) {
if (LOG_AI_ATTACKS)
System.out.println("Assault");
@@ -850,7 +843,6 @@ public class AiAttackController {
// no more creatures to attack
return;
}
-
// *******************
// Evaluate the creature forces
@@ -935,12 +927,9 @@ public class AiAttackController {
// *********************
// if outnumber and superior ratio work out whether attritional all out
- // attacking will work
- // attritional attack will expect some creatures to die but to achieve
- // victory by sheer weight
- // of numbers attacking turn after turn. It's not calculate very
- // carefully, the accuracy
- // can probably be improved
+ // attacking will work attritional attack will expect some creatures to die but to achieve
+ // victory by sheer weight of numbers attacking turn after turn. It's not calculate very
+ // carefully, the accuracy can probably be improved
// *********************
boolean doAttritionalAttack = false;
// get list of attackers ordered from low power to high
@@ -974,7 +963,6 @@ public class AiAttackController {
doAttritionalAttack = true;
}
}
- // System.out.println(doAttritionalAttack + " = do attritional attack");
// *********************
// end attritional attack calculation
// *********************
@@ -1024,11 +1012,9 @@ public class AiAttackController {
// end see how long until unblockable attackers will be fatal
// *****************
-
// decide on attack aggression based on a comparison of forces, life
- // totals and other considerations
- // some bad "magic numbers" here, TODO replace with nice descriptive
- // variable names
+ // totals and other considerations some bad "magic numbers" here
+ // TODO replace with nice descriptive variable names
if (ratioDiff > 0 && doAttritionalAttack) {
this.aiAggression = 5; // attack at all costs
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
@@ -1223,9 +1209,8 @@ public class AiAttackController {
}
// look at the attacker in relation to the blockers to establish a
- // number of factors about the attacking
- // context that will be relevant to the attackers decision according to
- // the selected strategy
+ // number of factors about the attacking context that will be relevant
+ // to the attackers decision according to the selected strategy
for (final Card defender : validBlockers) {
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
@@ -1299,14 +1284,11 @@ public class AiAttackController {
}
if (numberOfPossibleBlockers > 2
- || (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, combat))
- || (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, combat))) {
+ || (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, this.defendingOpponent))
+ || (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, this.defendingOpponent))) {
canBeBlocked = true;
}
- /*System.out.println(attacker + " canBeKilledByOne: " + canBeKilledByOne + " canKillAll: "
- + canKillAll + " isWorthLessThanAllKillers: " + isWorthLessThanAllKillers + " canBeBlocked: " + canBeBlocked);*/
- // decide if the creature should attack based on the prevailing strategy
- // choice in aiAggression
+ // decide if the creature should attack based on the prevailing strategy choice in aiAggression
switch (this.aiAggression) {
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) {
@@ -1441,7 +1423,7 @@ public class AiAttackController {
public String toProtectAttacker(SpellAbility sa) {
//AiAttackController is created with the selected attacker as the only entry in "attackers"
if (sa.getApi() != ApiType.Protection || oppList.isEmpty() || getPossibleBlockers(oppList, attackers).isEmpty()) {
- return null; //not protection sa or attacker is already unblockable
+ return null; //not protection sa or attacker is already unblockable
}
final List choices = ProtectEffect.getProtectionList(sa);
String color = ComputerUtilCard.getMostProminentColor(getPossibleBlockers(oppList, attackers)), artifact = null;
@@ -1451,7 +1433,7 @@ public class AiAttackController {
if (!choices.contains(color)) {
color = null;
}
- for (Card c : oppList) { //find a blocker that ignores the currently selected protection
+ for (Card c : oppList) { //find a blocker that ignores the currently selected protection
if (artifact != null && !c.isArtifact()) {
artifact = null;
}
@@ -1484,7 +1466,7 @@ public class AiAttackController {
break;
}
}
- if (color == null && artifact == null) { //nothing can make the attacker unblockable
+ if (color == null && artifact == null) { //nothing can make the attacker unblockable
return null;
}
}
@@ -1562,4 +1544,4 @@ public class AiAttackController {
return true;
}
-} // end class ComputerUtil_Attack2
+}
diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java
index ca567fdcb4a..a6b16cb648d 100644
--- a/forge-ai/src/main/java/forge/ai/AiBlockController.java
+++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java
@@ -180,11 +180,9 @@ public class AiBlockController {
// Good Blocks means a good trade or no trade
private void makeGoodBlocks(final Combat combat) {
-
List currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : attackersLeft) {
-
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|| attacker.hasKeyword(Keyword.MENACE)) {
@@ -192,7 +190,6 @@ public class AiBlockController {
}
Card blocker = null;
-
final List blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
final List safeBlockers = getSafeBlockers(combat, attacker, blockers);
@@ -305,7 +302,6 @@ public class AiBlockController {
}
Card blocker = null;
-
final List blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
for (Card b : blockers) {
@@ -366,10 +362,9 @@ public class AiBlockController {
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
// if the total damage of the blockgang was not enough
- // without but is enough with this blocker finish the
- // blockgang
+ // without but is enough with this blocker finish the blockgang
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) < damageNeeded
- || CombatUtil.needsBlockers(attacker) > blockGang.size()) {
+ || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size()) {
blockGang.add(blocker);
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) >= damageNeeded) {
currentAttackers.remove(attacker);
@@ -407,7 +402,7 @@ public class AiBlockController {
boolean foundDoubleBlock = false; // if true, a good double block is found
// AI can't handle good blocks with more than three creatures yet
- if (CombatUtil.needsBlockers(attacker) > (considerTripleBlock ? 3 : 2)) {
+ if (CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > (considerTripleBlock ? 3 : 2)) {
continue;
}
@@ -444,7 +439,7 @@ public class AiBlockController {
final int addedValue = ComputerUtilCard.evaluateCreature(blocker);
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
- if ((damageNeeded > currentDamage || CombatUtil.needsBlockers(attacker) > blockGang.size())
+ if ((damageNeeded > currentDamage || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size())
&& !(damageNeeded > currentDamage + additionalDamage)
// The attacker will be killed
&& (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage()
@@ -454,8 +449,7 @@ public class AiBlockController {
|| (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)))
// or life is in danger
&& CombatUtil.canBlock(attacker, blocker, combat)) {
- // this is needed for attackers that can't be blocked by
- // more than 1
+ // this is needed for attackers that can't be blocked by more than 1
currentAttackers.remove(attacker);
combat.addBlocker(attacker, blocker);
if (CombatUtil.canBlock(attacker, leader, combat)) {
@@ -496,7 +490,7 @@ public class AiBlockController {
final int addedValue3 = ComputerUtilCard.evaluateCreature(secondBlocker);
final int netCombatDamage = attacker.getNetCombatDamage();
- if ((damageNeeded > currentDamage || CombatUtil.needsBlockers(attacker) > blockGang.size())
+ if ((damageNeeded > currentDamage || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size())
&& !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3)
// The attacker will be killed
&& ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage
@@ -510,8 +504,7 @@ public class AiBlockController {
// or life is in danger
&& CombatUtil.canBlock(attacker, secondBlocker, combat)
&& CombatUtil.canBlock(attacker, thirdBlocker, combat)) {
- // this is needed for attackers that can't be blocked by
- // more than 1
+ // this is needed for attackers that can't be blocked by more than 1
currentAttackers.remove(attacker);
combat.addBlocker(attacker, thirdBlocker);
if (CombatUtil.canBlock(attacker, secondBlocker, combat)) {
@@ -587,12 +580,10 @@ public class AiBlockController {
* @param combat a {@link forge.game.combat.Combat} object.
*/
private void makeTradeBlocks(final Combat combat) {
-
List currentAttackers = new ArrayList<>(attackersLeft);
List killingBlockers;
for (final Card attacker : attackersLeft) {
-
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
@@ -628,7 +619,6 @@ public class AiBlockController {
// Chump Blocks (should only be made if life is in danger)
private void makeChumpBlocks(final Combat combat) {
-
List currentAttackers = new ArrayList<>(attackersLeft);
makeChumpBlocks(combat, currentAttackers);
@@ -639,7 +629,6 @@ public class AiBlockController {
}
private void makeChumpBlocks(final Combat combat, List attackers) {
-
if (attackers.isEmpty() || !ComputerUtilCombat.lifeInDanger(ai, combat)) {
return;
}
@@ -694,11 +683,9 @@ public class AiBlockController {
// Block creatures with "can't be blocked except by two or more creatures"
private void makeMultiChumpBlocks(final Combat combat) {
-
List currentAttackers = new ArrayList<>(attackersLeft);
for (final Card attacker : currentAttackers) {
-
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
&& !attacker.hasKeyword(Keyword.MENACE)
&& !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
@@ -730,14 +717,12 @@ public class AiBlockController {
/** Reinforce blockers blocking attackers with trample (should only be made if life is in danger) */
private void reinforceBlockersAgainstTrample(final Combat combat) {
-
List chumpBlockers;
List tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock));
- // TODO - should check here for a "rampage-like" trigger that replaced
- // the keyword:
+ // TODO - should check here for a "rampage-like" trigger that replaced the keyword:
// "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
for (final Card attacker : tramplingAttackers) {
@@ -764,7 +749,6 @@ public class AiBlockController {
/** Support blockers not destroying the attacker with more blockers to try to kill the attacker */
private void reinforceBlockersToKill(final Combat combat) {
-
List safeBlockers;
List blockers;
List targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock));
@@ -1036,27 +1020,21 @@ public class AiBlockController {
} else {
lifeInDanger = false;
}
- // if life is still in danger
- // Reinforce blockers blocking attackers with trample if life is
- // still
- // in danger
+ // Reinforce blockers blocking attackers with trample if life is still in danger
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat);
} else {
lifeInDanger = false;
}
// Support blockers not destroying the attacker with more blockers
- // to
- // try to kill the attacker
+ // to try to kill the attacker
if (!lifeInDanger) {
reinforceBlockersToKill(combat);
}
- // == 2. If the AI life would still be in danger make a safer
- // approach ==
+ // == 2. If the AI life would still be in danger make a safer approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
- clearBlockers(combat, possibleBlockers); // reset every block
- // assignment
+ clearBlockers(combat, possibleBlockers); // reset every block assignment
makeTradeBlocks(combat); // choose necessary trade blocks
// if life is in danger
makeGoodBlocks(combat);
@@ -1066,8 +1044,7 @@ public class AiBlockController {
} else {
lifeInDanger = false;
}
- // Reinforce blockers blocking attackers with trample if life is
- // still in danger
+ // Reinforce blockers blocking attackers with trample if life is still in danger
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
reinforceBlockersAgainstTrample(combat);
} else {
@@ -1077,11 +1054,9 @@ public class AiBlockController {
reinforceBlockersToKill(combat);
}
- // == 3. If the AI life would be in serious danger make an even
- // safer approach ==
+ // == 3. If the AI life would be in serious danger make an even safer approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
- clearBlockers(combat, possibleBlockers); // reset every block
- // assignment
+ clearBlockers(combat, possibleBlockers); // reset every block assignment
makeChumpBlocks(combat); // choose chump blocks
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeTradeBlocks(combat); // choose necessary trade
@@ -1090,15 +1065,13 @@ public class AiBlockController {
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeGoodBlocks(combat);
}
- // Reinforce blockers blocking attackers with trample if life is
- // still in danger
+ // Reinforce blockers blocking attackers with trample if life is still in danger
else {
reinforceBlockersAgainstTrample(combat);
}
makeGangBlocks(combat);
// Support blockers not destroying the attacker with more
- // blockers
- // to try to kill the attacker
+ // blockers to try to kill the attacker
reinforceBlockersToKill(combat);
}
}
@@ -1108,7 +1081,7 @@ public class AiBlockController {
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
// if an attacker with lure attacks - all that can block
for (final Card blocker : blockersLeft) {
- if (CombatUtil.mustBlockAnAttacker(blocker, combat)) {
+ if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) {
chumpBlockers.add(blocker);
}
}
@@ -1118,7 +1091,7 @@ public class AiBlockController {
blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false);
for (final Card blocker : blockers) {
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
- && (CombatUtil.mustBlockAnAttacker(blocker, combat)
+ && (CombatUtil.mustBlockAnAttacker(blocker, combat, null)
|| blocker.hasKeyword("CARDNAME blocks each turn if able.")
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
combat.addBlocker(attacker, blocker);
@@ -1137,7 +1110,6 @@ public class AiBlockController {
}
}
-
// check to see if it's possible to defend a Planeswalker under attack with a chump block,
// unless life is low enough to be more worried about saving preserving the life total
if (ai.getController().isAI() && !ComputerUtilCombat.lifeInDanger(ai, combat)) {
diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java
index ede9f756fde..e1a9e70e8da 100644
--- a/forge-ai/src/main/java/forge/ai/AiController.java
+++ b/forge-ai/src/main/java/forge/ai/AiController.java
@@ -34,6 +34,7 @@ import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.ExploreAi;
import forge.ai.ability.LearnAi;
import forge.ai.simulation.SpellAbilityPicker;
+import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.deck.CardPool;
@@ -46,6 +47,7 @@ import forge.game.Game;
import forge.game.GameActionUtil;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
+import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellApiBased;
@@ -78,6 +80,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplaceMoved;
import forge.game.replacement.ReplacementEffect;
+import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.LandAbility;
@@ -307,6 +310,8 @@ public class AiController {
}
exSA.setTrigger(tr);
+ // need to set TriggeredObject
+ exSA.setTriggeringObject(AbilityKey.Card, card);
// for trigger test, need to ignore the conditions
SpellAbilityCondition cons = exSA.getConditions();
@@ -432,17 +437,22 @@ public class AiController {
}
}
- // don't play the land if it has cycling and enough lands are available
- final FCollectionView spellAbilities = c.getSpellAbilities();
-
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
CardCollection lands = new CardCollection(battlefield);
lands.addAll(hand);
lands = CardLists.filter(lands, CardPredicates.Presets.LANDS);
int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc);
- for (final SpellAbility sa : spellAbilities) {
- if (sa.isCycling()) {
- if (lands.size() >= Math.max(maxCmcInHand, 6)) {
+
+ if (lands.size() >= Math.max(maxCmcInHand, 6)) {
+ // don't play MDFC land if other side is spell and enough lands are available
+ if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
+ return false;
+ }
+
+ // don't play the land if it has cycling and enough lands are available
+ final FCollectionView spellAbilities = c.getSpellAbilities();
+ for (final SpellAbility sa : spellAbilities) {
+ if (sa.isCycling()) {
return false;
}
}
@@ -503,20 +513,35 @@ public class AiController {
//try to skip lands that enter the battlefield tapped
if (!nonLandsInHand.isEmpty()) {
- CardCollection nonTappeddLands = new CardCollection();
+ CardCollection nonTappedLands = new CardCollection();
for (Card land : landList) {
- // Is this the best way to check if a land ETB Tapped?
- if (land.hasSVar("ETBTappedSVar")) {
+ // check replacement effects if land would enter tapped or not
+ final Map repParams = AbilityKey.mapFromAffected(land);
+ repParams.put(AbilityKey.Origin, land.getZone().getZoneType());
+ repParams.put(AbilityKey.Destination, ZoneType.Battlefield);
+ repParams.put(AbilityKey.Source, land);
+
+ boolean foundTapped = false;
+ for (ReplacementEffect re : player.getGame().getReplacementHandler().getReplacementList(ReplacementType.Moved, repParams, ReplacementLayer.Other)) {
+ SpellAbility reSA = re.ensureAbility();
+ if (reSA == null || !ApiType.Tap.equals(reSA.getApi())) {
+ continue;
+ }
+ reSA.setActivatingPlayer(reSA.getHostCard().getController());
+ if (reSA.metConditions()) {
+ foundTapped = true;
+ break;
+ }
+ }
+
+ if (foundTapped) {
continue;
}
- // Glacial Fortress and friends
- if (land.hasSVar("ETBCheckSVar") && CardFactoryUtil.xCount(land, land.getSVar("ETBCheckSVar")) == 0) {
- continue;
- }
- nonTappeddLands.add(land);
+
+ nonTappedLands.add(land);
}
- if (!nonTappeddLands.isEmpty()) {
- landList = nonTappeddLands;
+ if (!nonTappedLands.isEmpty()) {
+ landList = nonTappedLands;
}
}
@@ -587,8 +612,7 @@ public class AiController {
SpellAbility currentSA = sa;
sa.setActivatingPlayer(player);
// check everything necessary
-
-
+
AiPlayDecision opinion = canPlayAndPayFor(currentSA);
//PhaseHandler ph = game.getPhaseHandler();
// System.out.printf("Ai thinks '%s' of %s @ %s %s >>> \n", opinion, sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase());
@@ -1301,15 +1325,6 @@ public class AiController {
}
public boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message) {
- if (logic.equalsIgnoreCase("ProtectFriendly")) {
- final Player controller = hostCard.getController();
- if (affected instanceof Player) {
- return !((Player) affected).isOpponentOf(controller);
- }
- if (affected instanceof Card) {
- return !((Card) affected).getController().isOpponentOf(controller);
- }
- }
return true;
}
@@ -1408,8 +1423,6 @@ public class AiController {
private List singleSpellAbilityList(SpellAbility sa) {
if (sa == null) { return null; }
- // System.out.println("Chosen to play: " + sa);
-
final List abilities = Lists.newArrayList();
abilities.add(sa);
return abilities;
@@ -1454,6 +1467,13 @@ public class AiController {
if (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land)) {
final List abilities = Lists.newArrayList();
+ // TODO extend this logic to evaluate MDFC with both sides land
+ // this can only happen if its a MDFC land
+ if (!land.isLand()) {
+ land.setState(CardStateName.Modal, true);
+ land.setBackSide(true);
+ }
+
LandAbility la = new LandAbility(land, player, null);
la.setCardState(land.getCurrentState());
if (la.canPlay()) {
@@ -1704,10 +1724,8 @@ public class AiController {
for (int i = 0; i < numToExile; i++) {
Card chosen = null;
for (final Card c : grave) { // Exile noncreatures first in
- // case we can revive. Might
- // wanna do some additional
- // checking here for Flashback
- // and the like.
+ // case we can revive. Might wanna do some additional
+ // checking here for Flashback and the like.
if (!c.isCreature()) {
chosen = c;
break;
@@ -1746,12 +1764,21 @@ public class AiController {
* @param sa the sa
* @return true, if successful
*/
- public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa) {
+ public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
Card hostCard = effect.getHostCard();
if (hostCard.hasAlternateState()) {
hostCard = game.getCardState(hostCard);
}
+ if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
+ final Player controller = hostCard.getController();
+ if (affected instanceof Player) {
+ return !((Player) affected).isOpponentOf(controller);
+ }
+ if (affected instanceof Card) {
+ return !((Card) affected).getController().isOpponentOf(controller);
+ }
+ }
if (effect.hasParam("AICheckSVar")) {
System.out.println("aiShouldRun?" + sa);
final String svarToCheck = effect.getParam("AICheckSVar");
@@ -1766,7 +1793,7 @@ public class AiController {
compareTo = Integer.parseInt(strCmpTo);
} catch (final Exception ignored) {
if (sa == null) {
- compareTo = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(strCmpTo));
+ compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
} else {
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
}
@@ -1776,7 +1803,7 @@ public class AiController {
int left = 0;
if (sa == null) {
- left = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(svarToCheck));
+ left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
} else {
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
}
@@ -1866,11 +1893,13 @@ public class AiController {
} else if ("LowestLoseLife".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
} else if ("HighestLoseLife".equals(logic)) {
- return MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
+ return Math.min(player.getLife() -1,MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1);
} else if ("HighestGetCounter".equals(logic)) {
return MyRandom.getRandom().nextInt(3);
} else if (source.hasSVar("EnergyToPay")) {
return AbilityUtils.calculateAmount(source, source.getSVar("EnergyToPay"), sa);
+ } else if ("Vermin".equals(logic)) {
+ return MyRandom.getRandom().nextInt(Math.max(player.getLife() - 5, 0));
}
return max;
}
@@ -1965,7 +1994,6 @@ public class AiController {
return result;
}
-
// this is where the computer cheats
// changes AllZone.getComputerPlayer().getZone(Zone.Library)
@@ -2245,10 +2273,8 @@ public class AiController {
}
}
- // AI logic for choosing which replacement effect to apply
- // happens here.
+ // AI logic for choosing which replacement effect to apply happens here.
return Iterables.getFirst(list, null);
}
}
-
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
index 17fb2007fab..99f2234841e 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
@@ -280,7 +280,7 @@ public class ComputerUtil {
SpellAbility newSA = sa.copyWithNoManaCost();
newSA.setActivatingPlayer(ai);
- if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA)) {
+ if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0)) {
return false;
}
@@ -1125,7 +1125,7 @@ public class ComputerUtil {
creatures2.add(creatures.get(i));
}
}
- if (((creatures2.size() + CardUtil.getThisTurnCast("Creature.YouCtrl", vengevines.get(0)).size()) > 1)
+ if (((creatures2.size() + CardUtil.getThisTurnCast("Creature.YouCtrl", vengevines.get(0), null).size()) > 1)
&& card.isCreature() && card.getManaCost().getCMC() <= 3) {
return true;
}
@@ -2253,10 +2253,8 @@ public class ComputerUtil {
String chosen = "";
if (kindOfType.equals("Card")) {
// TODO
- // computer will need to choose a type
- // based on whether it needs a creature or land,
- // otherwise, lib search for most common type left
- // then, reveal chosenType to Human
+ // computer will need to choose a type based on whether it needs a creature or land,
+ // otherwise, lib search for most common type left then, reveal chosenType to Human
if (game.getPhaseHandler().is(PhaseType.UNTAP) && logic == null) { // Storage Matrix
double amount = 0;
for (String type : CardType.getAllCardTypes()) {
@@ -2434,8 +2432,7 @@ public class ComputerUtil {
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Feather" : "Quill";
}
- // if source is not on the battlefield anymore, choose +1/+1
- // ones
+ // if source is not on the battlefield anymore, choose +1/+1 ones
if (!game.getCardState(source).isInZone(ZoneType.Battlefield)) {
return opponent ? "Feather" : "Quill";
}
@@ -2477,8 +2474,7 @@ public class ComputerUtil {
return opponent ? "Numbers" : "Strength";
}
- // TODO check for ETB to +1/+1 counters
- // or over another trigger like lifegain
+ // TODO check for ETB to +1/+1 counters or over another trigger like lifegain
int tokenScore = ComputerUtilCard.evaluateCreature(token);
@@ -2556,8 +2552,7 @@ public class ComputerUtil {
return "Taxes";
} else {
// ai is first voter or ally of controller
- // both are not affected, but if opponents controll creatures,
- // sacrifice is worse
+ // both are not affected, but if opponents control creatures, sacrifice is worse
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
}
default:
@@ -2854,7 +2849,6 @@ public class ComputerUtil {
}
public static boolean lifegainNegative(final Player player, final Card source, final int n) {
-
if (!player.canGainLife()) {
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
index 19172312c04..b710879376f 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
@@ -76,7 +76,7 @@ public class ComputerUtilCard {
}
});
}
- return ComputerUtilCard.getMostExpensivePermanentAI(all);
+ return getMostExpensivePermanentAI(all);
}
/**
@@ -265,16 +265,14 @@ public class ComputerUtilCard {
* @return a {@link forge.game.card.Card} object.
*/
public static Card getBestAI(final Iterable list) {
- // Get Best will filter by appropriate getBest list if ALL of the list
- // is of that type
+ // Get Best will filter by appropriate getBest list if ALL of the list is of that type
if (Iterables.all(list, CardPredicates.Presets.CREATURES)) {
return ComputerUtilCard.getBestCreatureAI(list);
}
if (Iterables.all(list, CardPredicates.Presets.LANDS)) {
return getBestLandAI(list);
}
- // TODO - Once we get an EvaluatePermanent this should call
- // getBestPermanent()
+ // TODO - Once we get an EvaluatePermanent this should call getBestPermanent()
return ComputerUtilCard.getMostExpensivePermanentAI(list);
}
@@ -383,7 +381,7 @@ public class ComputerUtilCard {
}
if (biasLand && Iterables.any(list, CardPredicates.Presets.LANDS)) {
- return ComputerUtilCard.getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS));
+ return getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS));
}
final boolean hasCreatures = Iterables.any(list, CardPredicates.Presets.CREATURES);
@@ -393,7 +391,7 @@ public class ComputerUtilCard {
List lands = CardLists.filter(list, CardPredicates.Presets.LANDS);
if (lands.size() > 6) {
- return ComputerUtilCard.getWorstLand(lands);
+ return getWorstLand(lands);
}
if (hasEnchantmants || hasArtifacts) {
@@ -410,8 +408,7 @@ public class ComputerUtilCard {
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES));
}
- // Planeswalkers fall through to here, lands will fall through if there
- // aren't very many
+ // Planeswalkers fall through to here, lands will fall through if there aren't very many
return getCheapestPermanentAI(list, null, false);
}
@@ -444,7 +441,7 @@ public class ComputerUtilCard {
public static final Comparator EvaluateCreatureComparator = new Comparator() {
@Override
public int compare(final Card a, final Card b) {
- return ComputerUtilCard.evaluateCreature(b) - ComputerUtilCard.evaluateCreature(a);
+ return evaluateCreature(b) - evaluateCreature(a);
}
};
@@ -882,9 +879,9 @@ public class ComputerUtilCard {
public static final Predicate AI_KNOWS_HOW_TO_PLAY_ALL_CARDS = new Predicate() {
@Override
public boolean apply(Deck d) {
- for(Entry cp: d) {
- for(Entry e : cp.getValue()) {
- if ( e.getKey().getRules().getAiHints().getRemAIDecks() )
+ for (Entry cp: d) {
+ for (Entry e : cp.getValue()) {
+ if (e.getKey().getRules().getAiHints().getRemAIDecks())
return false;
}
}
@@ -982,7 +979,7 @@ public class ComputerUtilCard {
for(byte c : MagicColor.WUBRG) {
String devotionCode = "Count$Devotion." + MagicColor.toLongString(c);
- int devotion = CardFactoryUtil.xCount(sa.getHostCard(), devotionCode);
+ int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), devotionCode, sa);
if (devotion > curDevotion && !CardLists.filter(hand, CardPredicates.isColor(c)).isEmpty()) {
curDevotion = devotion;
chosenColor = MagicColor.toLongString(c);
@@ -1198,7 +1195,7 @@ public class ComputerUtilCard {
}
String kws = params.get("AddKeyword");
if (kws != null) {
- bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
+ bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
}
if (bonusPT > 0) {
threat = bonusPT * (1 + opp.getCreaturesInPlay().size()) / 10.0f;
@@ -1212,7 +1209,7 @@ public class ComputerUtilCard {
}
final float valueNow = Math.max(valueTempo, threat);
- if (valueNow < 0.2) { //hard floor to reduce ridiculous odds for instants over time
+ if (valueNow < 0.2) { //hard floor to reduce ridiculous odds for instants over time
return false;
}
final float chance = MyRandom.getRandom().nextFloat();
@@ -1310,7 +1307,7 @@ public class ComputerUtilCard {
threat *= 2;
}
if (c.getNetPower() == 0 && c == sa.getHostCard() && power > 0 ) {
- threat *= 4; //over-value self +attack for 0 power creatures which may be pumped further after attacking
+ threat *= 4; //over-value self +attack for 0 power creatures which may be pumped further after attacking
}
chance += threat;
@@ -1644,11 +1641,12 @@ public class ComputerUtilCard {
copiedKeywords.insertAll(pumped.getKeywords());
List toCopy = Lists.newArrayList();
for (KeywordInterface k : c.getKeywords()) {
- if (!copiedKeywords.contains(k.getOriginal())) {
- if (k.getHidden()) {
- pumped.addHiddenExtrinsicKeyword(k);
+ KeywordInterface copiedKI = k.copy(c, true);
+ if (!copiedKeywords.contains(copiedKI.getOriginal())) {
+ if (copiedKI.getHidden()) {
+ pumped.addHiddenExtrinsicKeyword(copiedKI);
} else {
- toCopy.add(k);
+ toCopy.add(copiedKI);
}
}
}
@@ -1678,37 +1676,27 @@ public class ComputerUtilCard {
// remove old boost that might be copied
for (final StaticAbility stAb : c.getStaticAbilities()) {
vCard.removePTBoost(c.getTimestamp(), stAb.getId());
- final Map params = stAb.getMapParams();
- if (!params.get("Mode").equals("Continuous")) {
+ if (!stAb.getParam("Mode").equals("Continuous")) {
continue;
}
- if (!params.containsKey("Affected")) {
+ if (!stAb.hasParam("Affected")) {
continue;
}
- if (!params.containsKey("AddPower") && !params.containsKey("AddToughness")) {
+ if (!stAb.hasParam("AddPower") && !stAb.hasParam("AddToughness")) {
continue;
}
- final String valid = params.get("Affected");
- if (!vCard.isValid(valid, c.getController(), c, null)) {
+ if (!vCard.isValid(stAb.getParam("Affected").split(","), c.getController(), c, stAb)) {
continue;
}
int att = 0;
- if (params.containsKey("AddPower")) {
- String addP = params.get("AddPower");
- if (addP.equals("AffectedX")) {
- att = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addP));
- } else {
- att = AbilityUtils.calculateAmount(c, addP, stAb);
- }
+ if (stAb.hasParam("AddPower")) {
+ String addP = stAb.getParam("AddPower");
+ att = AbilityUtils.calculateAmount(addP.startsWith("Affected") ? vCard : c, addP, stAb, true);
}
int def = 0;
- if (params.containsKey("AddToughness")) {
- String addT = params.get("AddToughness");
- if (addT.equals("AffectedY")) {
- def = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addT));
- } else {
- def = AbilityUtils.calculateAmount(c, addT, stAb);
- }
+ if (stAb.hasParam("AddToughness")) {
+ String addT = stAb.getParam("AddToughness");
+ def = AbilityUtils.calculateAmount(addT.startsWith("Affected") ? vCard : c, addT, stAb, true);
}
vCard.addPTBoost(att, def, c.getTimestamp(), stAb.getId());
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
index ebcde781c0e..148952c0840 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
@@ -33,7 +33,6 @@ import forge.game.ability.ApiType;
import forge.game.card.Card;
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.CardUtil;
@@ -335,7 +334,7 @@ public class ComputerUtilCombat {
*/
public static int resultingPoison(final Player ai, final Combat combat) {
- // ai can't get poision counters, so the value can't change
+ // ai can't get poison counters, so the value can't change
if (!ai.canReceiveCounters(CounterEnumType.POISON)) {
return ai.getPoisonCounters();
}
@@ -413,7 +412,6 @@ public class ComputerUtilCombat {
return false;
}
-
// check for creatures that must be blocked
final List attackers = combat.getAttackersOf(ai);
@@ -493,8 +491,7 @@ public class ComputerUtilCombat {
}
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat, final int payment) {
- // life in danger only cares about the player's life. Not about a
- // Planeswalkers life
+ // life in danger only cares about the player's life. Not about a Planeswalkers life
if (ai.cantLose() || combat == null) {
return false;
}
@@ -569,8 +566,7 @@ public class ComputerUtilCombat {
return damage;
}
- // This calculates the amount of damage a blocker in a blockgang can deal to
- // the attacker
+ // This calculates the amount of damage a blocker in a blockgang can deal to the attacker
/**
*
* dealsDamageAsBlocker.
@@ -583,7 +579,6 @@ public class ComputerUtilCombat {
* @return a int.
*/
public static int dealsDamageAsBlocker(final Card attacker, final Card defender) {
-
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
@@ -648,7 +643,6 @@ public class ComputerUtilCombat {
* @return a int.
*/
public static int totalShieldDamage(final Card attacker, final List defenders) {
-
int defenderDefense = 0;
for (final Card defender : defenders) {
@@ -672,7 +666,6 @@ public class ComputerUtilCombat {
* @return a int.
*/
public static int shieldDamage(final Card attacker, final Card blocker) {
-
if (ComputerUtilCombat.canDestroyBlockerBeforeFirstStrike(blocker, attacker, false)) {
return 0;
}
@@ -777,7 +770,6 @@ public class ComputerUtilCombat {
public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger,
Combat combat, final List plannedAttackers) {
final Game game = attacker.getGame();
- final Map trigParams = trigger.getMapParams();
boolean willTrigger = false;
final Card source = trigger.getHostCard();
if (combat == null) {
@@ -800,29 +792,27 @@ public class ComputerUtilCombat {
if (combat.isAttacking(attacker)) {
return false; // The trigger should have triggered already
}
- if (trigParams.containsKey("ValidCard")) {
- if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))
- && !(combat.isAttacking(source) && trigger.matchesValid(source,
- trigParams.get("ValidCard").split(","))
- && !trigParams.containsKey("Alone"))) {
+ if (trigger.hasParam("ValidCard")) {
+ if (!trigger.matchesValidParam("ValidCard", attacker)
+ && !(combat.isAttacking(source) && trigger.matchesValidParam("ValidCard", source)
+ && !trigger.hasParam("Alone"))) {
return false;
}
}
- if (trigParams.containsKey("Attacked")) {
- if (combat.isAttacking(attacker)) {
- GameEntity attacked = combat.getDefenderByAttacker(attacker);
- if (!trigger.matchesValid(attacked, trigParams.get("Attacked").split(","))) {
- return false;
- }
- } else {
- if ("You,Planeswalker.YouCtrl".equals(trigParams.get("Attacked"))) {
- if (source.getController() == attacker.getController()) {
- return false;
- }
- }
- }
+ if (trigger.hasParam("Attacked")) {
+ if (combat.isAttacking(attacker)) {
+ if (!trigger.matchesValidParam("Attacked", combat.getDefenderByAttacker(attacker))) {
+ return false;
+ }
+ } else {
+ if ("You,Planeswalker.YouCtrl".equals(trigger.getParam("Attacked"))) {
+ if (source.getController() == attacker.getController()) {
+ return false;
+ }
+ }
+ }
}
- if (trigParams.containsKey("Alone") && plannedAttackers != null && plannedAttackers.size() != 1) {
+ if (trigger.hasParam("Alone") && plannedAttackers != null && plannedAttackers.size() != 1) {
return false; // won't trigger since the AI is planning to attack with more than one creature
}
}
@@ -830,10 +820,8 @@ public class ComputerUtilCombat {
// defender == null means unblocked
if ((defender == null) && mode == TriggerType.AttackerUnblocked) {
willTrigger = true;
- if (trigParams.containsKey("ValidCard")) {
- if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))) {
- return false;
- }
+ if (!trigger.matchesValidParam("ValidCard", attacker)) {
+ return false;
}
}
@@ -843,8 +831,8 @@ public class ComputerUtilCombat {
if (mode == TriggerType.Blocks) {
willTrigger = true;
- if (trigParams.containsKey("ValidBlocked")) {
- String validBlocked = trigParams.get("ValidBlocked");
+ if (trigger.hasParam("ValidBlocked")) {
+ String validBlocked = trigger.getParam("ValidBlocked");
if (validBlocked.contains(".withLesserPower")) {
// Have to check this restriction here as triggering objects aren't set yet, so
// ValidBlocked$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE
@@ -857,8 +845,8 @@ public class ComputerUtilCombat {
return false;
}
}
- if (trigParams.containsKey("ValidCard")) {
- String validBlocker = trigParams.get("ValidCard");
+ if (trigger.hasParam("ValidCard")) {
+ String validBlocker = trigger.getParam("ValidCard");
if (validBlocker.contains(".withLesserPower")) {
// Have to check this restriction here as triggering objects aren't set yet, so
// ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE
@@ -873,29 +861,23 @@ public class ComputerUtilCombat {
}
} else if (mode == TriggerType.AttackerBlocked || mode == TriggerType.AttackerBlockedByCreature) {
willTrigger = true;
- if (trigParams.containsKey("ValidBlocker")) {
- if (!trigger.matchesValid(defender, trigParams.get("ValidBlocker").split(","))) {
- return false;
- }
+ if (!trigger.matchesValidParam("ValidBlocker", defender)) {
+ return false;
}
- if (trigParams.containsKey("ValidCard")) {
- if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))) {
- return false;
- }
+ if (!trigger.matchesValidParam("ValidCard", attacker)) {
+ return false;
}
} else if (mode == TriggerType.DamageDone) {
willTrigger = true;
- if (trigParams.containsKey("ValidSource")) {
- if (!(trigger.matchesValid(defender, trigParams.get("ValidSource").split(","))
+ if (trigger.hasParam("ValidSource")) {
+ if (!(trigger.matchesValidParam("ValidSource", defender)
&& defender.getNetCombatDamage() > 0
- && (!trigParams.containsKey("ValidTarget")
- || trigger.matchesValid(attacker, trigParams.get("ValidTarget").split(","))))) {
+ && trigger.matchesValidParam("ValidTarget", attacker))) {
return false;
}
- if (!(trigger.matchesValid(attacker, trigParams.get("ValidSource").split(","))
+ if (!(trigger.matchesValidParam("ValidSource", attacker)
&& attacker.getNetCombatDamage() > 0
- && (!trigParams.containsKey("ValidTarget")
- || trigger.matchesValid(defender, trigParams.get("ValidTarget").split(","))))) {
+ && trigger.matchesValidParam("ValidTarget", defender))) {
return false;
}
}
@@ -944,30 +926,22 @@ public class ComputerUtilCombat {
}
final Game game = attacker.getGame();
- // look out for continuous static abilities that only care for blocking
- // creatures
+ // look out for continuous static abilities that only care for blocking creatures
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
- final Map params = stAb.getMapParams();
- if (!params.get("Mode").equals("Continuous")) {
+ if (!stAb.getParam("Mode").equals("Continuous")) {
continue;
}
- if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) {
+ if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
continue;
}
- final String valid = TextUtil.fastReplace(params.get("Affected"), "blocking", "Creature");
- if (!blocker.isValid(valid, card.getController(), card, null)) {
+ final String valid = TextUtil.fastReplace(stAb.getParam("Affected"), "blocking", "Creature");
+ if (!blocker.isValid(valid, card.getController(), card, stAb)) {
continue;
}
- if (params.containsKey("AddPower")) {
- if (params.get("AddPower").equals("X")) {
- power += CardFactoryUtil.xCount(card, card.getSVar("X"));
- } else if (params.get("AddPower").equals("Y")) {
- power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
- } else {
- power += Integer.valueOf(params.get("AddPower"));
- }
+ if (stAb.hasParam("AddPower")) {
+ power += AbilityUtils.calculateAmount(card, stAb.getParam("AddPower"), stAb);
}
}
}
@@ -1252,31 +1226,23 @@ public class ComputerUtilCombat {
theTriggers.addAll(blocker.getTriggers());
}
- // look out for continuous static abilities that only care for attacking
- // creatures
+ // look out for continuous static abilities that only care for attacking creatures
if (!withoutCombatStaticAbilities) {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
- final Map params = stAb.getMapParams();
- if (!params.get("Mode").equals("Continuous")) {
+ if (!stAb.getParam("Mode").equals("Continuous")) {
continue;
}
- if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
+ if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
continue;
}
- final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
- if (!attacker.isValid(valid, card.getController(), card, null)) {
+ final String valid = TextUtil.fastReplace(stAb.getParam("Affected"), "attacking", "Creature");
+ if (!attacker.isValid(valid, card.getController(), card, stAb)) {
continue;
}
- if (params.containsKey("AddPower")) {
- if (params.get("AddPower").equals("X")) {
- power += CardFactoryUtil.xCount(card, card.getSVar("X"));
- } else if (params.get("AddPower").equals("Y")) {
- power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
- } else {
- power += Integer.valueOf(params.get("AddPower"));
- }
+ if (stAb.hasParam("AddPower")) {
+ power += AbilityUtils.calculateAmount(card, stAb.getParam("AddPower"), stAb);
}
}
}
@@ -1350,7 +1316,7 @@ public class ComputerUtilCombat {
} else if (bonus.contains("TriggeredAttacker$CardToughness")) {
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardToughness", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetToughness())));
}
- power += CardFactoryUtil.xCount(source, bonus);
+ power += AbilityUtils.calculateAmount(source, bonus, sa);
}
}
@@ -1447,8 +1413,7 @@ public class ComputerUtilCombat {
theTriggers.addAll(blocker.getTriggers());
}
- // look out for continuous static abilities that only care for attacking
- // creatures
+ // look out for continuous static abilities that only care for attacking creatures
if (!withoutCombatStaticAbilities) {
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
for (final Card card : cardList) {
@@ -1540,7 +1505,7 @@ public class ComputerUtilCombat {
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
}
- toughness += CardFactoryUtil.xCount(source, bonus);
+ toughness += AbilityUtils.calculateAmount(source, bonus, sa);
}
} else if (ApiType.PumpAll.equals(sa.getApi())) {
@@ -1573,7 +1538,7 @@ public class ComputerUtilCombat {
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
}
- toughness += CardFactoryUtil.xCount(source, bonus);
+ toughness += AbilityUtils.calculateAmount(source, bonus, sa);
}
}
}
@@ -1638,8 +1603,8 @@ public class ComputerUtilCombat {
//Check triggers that deal damage or shrink the attacker
if (ComputerUtilCombat.getDamageToKill(attacker)
- + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities) <= 0) {
- return true;
+ + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities) <= 0) {
+ return true;
}
// check Destroy triggers (Cockatrice and friends)
@@ -2256,11 +2221,12 @@ public class ComputerUtilCombat {
* @return a int.
*/
public final static int getDamageToKill(final Card c) {
- int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields();
+ int damageShield = c.getPreventNextDamageTotalShields();
+ int killDamage = c.getLethalDamage() + damageShield;
- if ((killDamage > c.getPreventNextDamageTotalShields())
+ if ((killDamage > damageShield)
&& c.hasSVar("DestroyWhenDamaged")) {
- killDamage = 1 + c.getPreventNextDamageTotalShields();
+ killDamage = 1 + damageShield;
}
return killDamage;
@@ -2282,7 +2248,6 @@ public class ComputerUtilCombat {
*/
public final static int predictDamageTo(final Player target, final int damage, final Card source, final boolean isCombat) {
-
final Game game = target.getGame();
int restDamage = damage;
@@ -2291,39 +2256,38 @@ public class ComputerUtilCombat {
// Predict replacement effects
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final ReplacementEffect re : ca.getReplacementEffects()) {
- Map params = re.getMapParams();
- if (!re.getMode().equals(ReplacementType.DamageDone) || !params.containsKey("PreventionEffect")) {
+ if (!re.getMode().equals(ReplacementType.DamageDone) ||
+ (!re.hasParam("PreventionEffect") && !re.hasParam("Prevent"))) {
continue;
}
// Immortal Coil prevents the damage but has a similar negative effect
if ("Immortal Coil".equals(ca.getName())) {
continue;
}
- if (params.containsKey("ValidSource")
- && !source.isValid(params.get("ValidSource"), ca.getController(), ca, null)) {
+ if (!re.matchesValidParam("ValidSource", source)) {
continue;
}
- if (params.containsKey("ValidTarget")
- && !target.isValid(params.get("ValidTarget"), ca.getController(), ca, null)) {
+ if (!re.matchesValidParam("ValidTarget", source)) {
continue;
}
- if (params.containsKey("IsCombat")) {
- if (params.get("IsCombat").equals("True")) {
- if (!isCombat) {
- continue;
- }
- } else {
- if (isCombat) {
- continue;
- }
+ if (re.hasParam("IsCombat")) {
+ if (re.getParam("IsCombat").equals("True") != isCombat) {
+ continue;
+ }
+ }
+ if (re.hasParam("Prevent")) {
+ return 0;
+ } else if (re.getOverridingAbility() != null) {
+ SpellAbility repSA = re.getOverridingAbility();
+ if (repSA.getApi() == ApiType.ReplaceDamage) {
+ return Math.max(0, restDamage - AbilityUtils.calculateAmount(ca, repSA.getParam("Amount"), repSA));
}
-
}
return 0;
}
}
- restDamage = target.staticDamagePrevention(restDamage, source, isCombat, true);
+ restDamage = target.staticDamagePrevention(restDamage, 0, source, isCombat);
return restDamage;
}
@@ -2344,13 +2308,7 @@ public class ComputerUtilCombat {
// This function helps the AI calculate the actual amount of damage an
// effect would deal
public final static int predictDamageTo(final Card target, final int damage, final Card source, final boolean isCombat) {
-
- int restDamage = damage;
-
- restDamage = target.staticReplaceDamage(restDamage, source, isCombat);
- restDamage = target.staticDamagePrevention(restDamage, source, isCombat, true);
-
- return restDamage;
+ return predictDamageTo(target, damage, 0, source, isCombat);
}
@@ -2372,7 +2330,6 @@ public class ComputerUtilCombat {
* @return a int.
*/
public final static int predictDamageTo(final Card target, final int damage, final int possiblePrevention, final Card source, final boolean isCombat) {
-
int restDamage = damage;
restDamage = target.staticReplaceDamage(restDamage, source, isCombat);
@@ -2382,7 +2339,6 @@ public class ComputerUtilCombat {
}
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
-
if (combatant.hasKeyword(Keyword.DOUBLE_STRIKE) || combatant.hasKeyword(Keyword.FIRST_STRIKE)) {
return true;
}
@@ -2490,7 +2446,14 @@ public class ComputerUtilCombat {
List list = game.getReplacementHandler().getReplacementList(
ReplacementType.DamageDone, repParams, ReplacementLayer.Other);
- return !list.isEmpty();
+ for (final ReplacementEffect re : list) {
+ Map params = re.getMapParams();
+ if (params.containsKey("Prevent") ||
+ (re.getOverridingAbility() != null && re.getOverridingAbility().getApi() != ApiType.ReplaceDamage && re.getOverridingAbility().getApi() != ApiType.ReplaceEffect)) {
+ return true;
+ }
+ }
+ return false;
}
public static boolean attackerHasThreateningAfflict(Card attacker, Player aiDefender) {
@@ -2499,20 +2462,6 @@ public class ComputerUtilCombat {
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
}
- public static int getMaxAttackersFor(final GameEntity defender) {
- if (defender instanceof Player) {
- for (final Card card : ((Player) defender).getCardsIn(ZoneType.Battlefield)) {
- if (card.hasKeyword("No more than one creature can attack you each combat.")) {
- return 1;
- } else if (card.hasKeyword("No more than two creatures can attack you each combat.")) {
- return 2;
- }
- }
- }
-
- return -1;
- }
-
public static List categorizeAttackersByEvasion(List attackers) {
List categorizedAttackers = Lists.newArrayList();
@@ -2602,5 +2551,3 @@ public class ComputerUtilCombat {
return false;
}
}
-
-
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
index f47319913a4..532764b0b1f 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
@@ -3,6 +3,7 @@ package forge.ai;
import java.util.List;
import java.util.Set;
+import forge.game.ability.ApiType;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@@ -156,7 +157,7 @@ public class ComputerUtilCost {
if (typeList.size() > ai.getMaxHandSize()) {
continue;
}
- int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
+ int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
@@ -557,7 +558,6 @@ public class ComputerUtilCost {
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
boolean payNever = "Never".equals(aiLogic);
- boolean shockland = "Shockland".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
@@ -572,26 +572,6 @@ public class ComputerUtilCost {
if (sa.getHostCard() == null || payer.equals(sa.getHostCard().getController())) {
return false;
}
- } else if (shockland) {
- if (payer.getLife() > 3 && payer.canPayLife(2)) {
- final int landsize = payer.getLandsInPlay().size() + 1;
- for (Card c : payer.getCardsIn(ZoneType.Hand)) {
- // if the new land size would equal the CMC of a card in AIs hand, consider playing it untapped,
- // otherwise don't bother running other checks
- if (landsize != c.getCMC()) {
- continue;
- }
- // try to determine in the AI is actually planning to play a spell ability from the card
- boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
- // try to determine if the mana shards provided by the lands would be applicable to pay the mana cost
- boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
-
- if (canPay && willPlay) {
- return true;
- }
- }
- }
- return false;
} else if ("Paralyze".equals(aiLogic)) {
final Card c = source.getEnchantingCard();
if (c == null || c.isUntapped()) {
@@ -629,6 +609,29 @@ public class ComputerUtilCost {
return false;
}
+ // Check for shocklands and similar ETB replacement effects
+ if (sa.hasParam("ETB") && sa.getApi().equals(ApiType.Tap)) {
+ for (final CostPart part : cost.getCostParts()) {
+ if (part instanceof CostPayLife) {
+ final CostPayLife lifeCost = (CostPayLife) part;
+ Integer amount = lifeCost.convertAmount();
+ if (payer.getLife() > (amount + 1) && payer.canPayLife(amount)) {
+ final int landsize = payer.getLandsInPlay().size() + 1;
+ for (Card c : payer.getCardsIn(ZoneType.Hand)) {
+ // Check if the AI has enough lands to play the card
+ if (landsize != c.getCMC()) {
+ continue;
+ }
+ // Check if the AI intends to play the card and if it can pay for it with the mana it has
+ boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
+ boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
+ return canPay && willPlay;
+ }
+ }
+ }
+ }
+ }
+
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
return false;
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
index d05d10c0111..b2dcbc7f6a0 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
@@ -609,7 +609,6 @@ public class ComputerUtilMana {
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
- //System.out.println(manaProduced);
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
index 917012b236d..201f1436785 100644
--- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
+++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
@@ -162,9 +162,9 @@ public class CreatureEvaluator implements Function {
value -= subValue(10, "must-attack");
} else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
value -= subValue(10, "must-attack-player");
- } else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
+ }/* else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
value -= subValue(toughness * 5, "reverse-reach");
- }
+ }//*/
if (c.hasSVar("DestroyWhenDamaged")) {
value -= subValue((toughness - 1) * 9, "dies-to-dmg");
diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java
index bc31f46fb98..40ea465041a 100644
--- a/forge-ai/src/main/java/forge/ai/GameState.java
+++ b/forge-ai/src/main/java/forge/ai/GameState.java
@@ -1215,8 +1215,7 @@ public abstract class GameState {
boolean tapped = c.isTapped();
boolean sickness = c.hasSickness();
Map counters = c.getCounters();
- // Note: Not clearCounters() since we want to keep the counters
- // var as-is.
+ // Note: Not clearCounters() since we want to keep the counters var as-is.
c.setCounters(Maps.newHashMap());
if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
index 2fe9bd55873..e1cd8b7da92 100644
--- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
+++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
@@ -81,9 +81,9 @@ import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
-/**
+/**
* A prototype for player controller class
- *
+ *
* Handles phase skips for now.
*/
public class PlayerControllerAi extends PlayerController {
@@ -94,11 +94,11 @@ public class PlayerControllerAi extends PlayerController {
brains = new AiController(p, game);
}
-
+
public void allowCheatShuffle(boolean value){
brains.allowCheatShuffle(value);
}
-
+
public void setUseSimulation(boolean value) {
brains.setUseSimulation(value);
}
@@ -131,6 +131,12 @@ public class PlayerControllerAi extends PlayerController {
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, damageDealt, defender, overrideOrder);
}
+ @Override
+ public Map divideShield(Card effectSource, Map affected, int shieldAmount) {
+ // TODO: AI current can't use this so this is not implemented.
+ return new HashMap<>();
+ }
+
@Override
public Integer announceRequirements(SpellAbility ability, String announce) {
// For now, these "announcements" are made within the AI classes of the appropriate SA effects
@@ -157,7 +163,7 @@ public class PlayerControllerAi extends PlayerController {
case BidLife:
return 0;
default:
- return null;
+ return null;
}
}
return null; // return incorrect value to indicate that
@@ -240,7 +246,7 @@ public class PlayerControllerAi extends PlayerController {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return getAi().confirmAction(sa, mode, message);
}
-
+
@Override
public boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode mode, String string,
int bid, Player winner) {
@@ -568,8 +574,8 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, String question) {
- return brains.aiShouldRun(replacementEffect, effectSA);
+ public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
+ return brains.aiShouldRun(replacementEffect, effectSA, affected);
}
@Override
@@ -646,10 +652,9 @@ public class PlayerControllerAi extends PlayerController {
public List chooseSpellAbilityToPlay() {
return brains.chooseSpellAbilityToPlay();
}
-
+
@Override
public boolean playChosenSpellAbility(SpellAbility sa) {
- // System.out.println("Playing sa: " + sa);
if (sa instanceof LandAbility) {
if (sa.canPlay()) {
sa.resolve();
@@ -659,7 +664,7 @@ public class PlayerControllerAi extends PlayerController {
ComputerUtil.handlePlayingSpellAbility(player, sa, getGame());
}
return true;
- }
+ }
@Override
public CardCollection chooseCardsToDiscardToMaximumHandSize(int numDiscard) {
@@ -712,7 +717,7 @@ public class PlayerControllerAi extends PlayerController {
public int chooseNumber(SpellAbility sa, String title, int min, int max) {
return brains.chooseNumber(sa, title, min, max);
}
-
+
@Override
public int chooseNumber(SpellAbility sa, String string, int min, int max, Map params) {
ApiType api = sa.getApi();
@@ -788,7 +793,8 @@ public class PlayerControllerAi extends PlayerController {
case "BetterTgtThanRemembered":
if (source.getRememberedCount() > 0) {
Card rem = (Card) source.getFirstRemembered();
- if (!rem.isInZone(ZoneType.Battlefield)) {
+ // avoid pumping opponent creature
+ if (!rem.isInZone(ZoneType.Battlefield) || rem.getController().isOpponentOf(source.getController())) {
return true;
}
for (Card c : source.getController().getCreaturesInPlay()) {
@@ -814,7 +820,7 @@ public class PlayerControllerAi extends PlayerController {
/*
* (non-Javadoc)
- *
+ *
* @see
* forge.game.player.PlayerController#chooseBinary(forge.game.spellability.
* SpellAbility, java.lang.String,
@@ -855,7 +861,7 @@ public class PlayerControllerAi extends PlayerController {
}
return sa.getChosenList();
}
-
+
@Override
public byte chooseColorAllowColorless(String message, Card card, ColorSet colors) {
final String c = ComputerUtilCard.getMostProminentColor(player.getCardsIn(ZoneType.Hand));
@@ -901,7 +907,7 @@ public class PlayerControllerAi extends PlayerController {
/*
* (non-Javadoc)
- *
+ *
* @see forge.game.player.PlayerController#chooseCounterType(java.util.List,
* forge.game.spellability.SpellAbility, java.lang.String, java.util.Map)
*/
@@ -917,7 +923,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean confirmPayment(CostPart costPart, String prompt, SpellAbility sa) {
- return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
+ return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
}
@Override
@@ -998,6 +1004,7 @@ public class PlayerControllerAi extends PlayerController {
emptyAbility.setActivatingPlayer(player);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
emptyAbility.setSVars(sa.getSVars());
+ emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid());
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) {
ComputerUtil.playNoStack(player, emptyAbility, getGame()); // AI needs something to resolve to pay that cost
return true;
@@ -1104,7 +1111,7 @@ public class PlayerControllerAi extends PlayerController {
public void revealAnte(String message, Multimap removedAnteCards) {
// Ai won't understand that anyway
}
-
+
@Override
public Collection extends PaperCard> complainCardsCantPlayWell(Deck myDeck) {
return brains.complainCardsCantPlayWell(myDeck);
@@ -1152,7 +1159,7 @@ public class PlayerControllerAi extends PlayerController {
return new HashMap<>();
}
}
-
+
//Do not convoke potential blockers until after opponent's attack
final CardCollectionView blockers = ComputerUtilCard.getLikelyBlockers(ai, null);
if ((ph.isPlayerTurn(ai) && ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN)) ||
diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
index 18ec64fc908..dcb194d663e 100644
--- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
@@ -39,7 +39,6 @@ import forge.game.ability.ApiType;
import forge.game.card.Card;
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.CardUtil;
@@ -379,7 +378,7 @@ public class SpecialCardAi {
}
// Do not activate if damage will be prevented
- if (source.staticDamagePrevention(predictedPT.getLeft(), source, true, true) == 0) {
+ if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
return false;
}
@@ -1085,7 +1084,7 @@ public class SpecialCardAi {
return false;
}
String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield));
- int devotion = CardFactoryUtil.xCount(sa.getHostCard(), "Count$Devotion." + prominentColor);
+ int devotion = AbilityUtils.calculateAmount(sa.getHostCard(), "Count$Devotion." + prominentColor, sa);
int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0);
// do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage)
diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
index 6b65082c41a..cab5c81ed2a 100644
--- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
@@ -162,7 +162,7 @@ public abstract class SpellAbilityAi {
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop
}
- return MyRandom.getRandom().nextFloat() < .8f; // random success
+ return MyRandom.getRandom().nextFloat() < .8f; // random success
}
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
@@ -193,7 +193,7 @@ public abstract class SpellAbilityAi {
* Handles the AI decision to play a triggered SpellAbility
*/
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
- if (canPlayWithoutRestrict(aiPlayer, sa)) {
+ if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
return true;
}
diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
index c95593f5c71..ae17f988dc7 100644
--- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
@@ -98,6 +98,7 @@ public enum SpellApiToAi {
.put(ApiType.Learn, LearnAi.class)
.put(ApiType.LoseLife, LifeLoseAi.class)
.put(ApiType.LosesGame, GameLossAi.class)
+ .put(ApiType.MakeCard, AlwaysPlayAi.class)
.put(ApiType.Mana, ManaEffectAi.class)
.put(ApiType.ManaReflected, CannotPlayAi.class)
.put(ApiType.Manifest, ManifestAi.class)
diff --git a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
index 0cb19367512..eb1e863f80b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
@@ -100,30 +100,30 @@ public class AnimateAi extends SpellAbilityAi {
}
// Don't use instant speed animate abilities before AI's COMBAT_BEGIN
if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
- && !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
+ && !sa.hasParam("ActivationPhases") && !"Permanent".equals(sa.getParam("Duration"))) {
return false;
}
// Don't use instant speed animate abilities outside human's
// COMBAT_DECLARE_ATTACKERS or if no attackers
- if (ph.getPlayerTurn().isOpponentOf(ai) && !sa.hasParam("Permanent")
+ if (ph.getPlayerTurn().isOpponentOf(ai) && !"Permanent".equals(sa.getParam("Duration"))
&& (!ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| game.getCombat() != null && game.getCombat().getAttackersOf(ai).isEmpty())) {
return false;
}
// Don't activate during MAIN2 unless this effect is permanent
- if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent") && !sa.hasParam("UntilYourNextTurn")) {
+ if (ph.is(PhaseType.MAIN2) && !"Permanent".equals(sa.getParam("Duration")) && !"UntilYourNextTurn".equals(sa.getParam("Duration"))) {
return false;
}
// Don't animate if the AI won't attack anyway or use as a potential blocker
Player opponent = ai.getWeakestOpponent();
// Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise
// the AI will waste resources
- boolean activateAsPotentialBlocker = sa.hasParam("UntilYourNextTurn")
+ boolean activateAsPotentialBlocker = "UntilYourNextTurn".equals(sa.getParam("Duration"))
&& ai.getGame().getPhaseHandler().getNextTurn() != ai
&& source.isPermanent();
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
&& Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
- && !sa.hasParam("AILogic") && !sa.hasParam("Permanent") && !activateAsPotentialBlocker) {
+ && !sa.hasParam("AILogic") && !"Permanent".equals(sa.getParam("Duration")) && !activateAsPotentialBlocker) {
return false;
}
return true;
@@ -156,7 +156,7 @@ public class AnimateAi extends SpellAbilityAi {
&& !c.isEquipping();
// for creatures that could be improved (like Figure of Destiny)
- if (!bFlag && c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
+ if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
@@ -179,7 +179,7 @@ public class AnimateAi extends SpellAbilityAi {
}
}
- if (!SpellAbilityAi.isSorcerySpeed(sa) && !sa.hasParam("Permanent")) {
+ if (!SpellAbilityAi.isSorcerySpeed(sa) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.hasParam("Crew") && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return false;
@@ -278,7 +278,7 @@ public class AnimateAi extends SpellAbilityAi {
Map data = Maps.newHashMap();
for (final Card c : list) {
// don't use Permanent animate on something that would leave the field
- if (c.hasSVar("EndOfTurnLeavePlay") && sa.hasParam("Permanent")) {
+ if (c.hasSVar("EndOfTurnLeavePlay") && "Permanent".equals(sa.getParam("Duration"))) {
continue;
}
@@ -312,9 +312,9 @@ public class AnimateAi extends SpellAbilityAi {
// if its player turn,
// check if its Permanent or that creature would attack
if (ph.isPlayerTurn(ai)) {
- if (!sa.hasParam("Permanent")
+ if (!"Permanent".equals(sa.getParam("Duration"))
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)
- && !sa.hasParam("UntilHostLeavesPlay")) {
+ && !"UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
continue;
}
}
@@ -360,8 +360,7 @@ public class AnimateAi extends SpellAbilityAi {
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
// that animate a target. Those can just use AI:RemoveDeck:All until
- // this can do a reasonably
- // good job of picking a good target
+ // this can do a reasonably good job of picking a good target
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
index 6d5f7f00076..90051e1cde9 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
@@ -555,12 +555,7 @@ public class AttachAi extends SpellAbilityAi {
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
- evenBetterList = CardLists.filter(betterList, new Predicate() {
- @Override
- public boolean apply(final Card c) {
- return c.isUntapped();
- }
- });
+ evenBetterList = CardLists.filter(betterList, CardPredicates.Presets.UNTAPPED);
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
@@ -734,17 +729,15 @@ public class AttachAi extends SpellAbilityAi {
*/
private static Card attachAISpecificCardPreference(final SpellAbility sa, final List list, final boolean mandatory,
final Card attachSource) {
- // I know this isn't much better than Hardcoding, but some cards need it for now
final Player ai = sa.getActivatingPlayer();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
Card chosen = null;
if ("Guilty Conscience".equals(sourceName)) {
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
- } else if ("Bonds of Faith".equals(sourceName)) {
- chosen = doPumpOrCurseAILogic(ai, sa, list, "Human");
- } else if ("Clutch of Undeath".equals(sourceName)) {
- chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
+ } else if (sa.hasParam("AIValid")) {
+ // TODO: Make the AI recognize which cards to pump based on the card's abilities alone
+ chosen = doPumpOrCurseAILogic(ai, sa, list, sa.getParam("AIValid"));
}
// If Mandatory (brought directly into play without casting) gotta
@@ -768,7 +761,7 @@ public class AttachAi extends SpellAbilityAi {
int powerBuff = 0;
for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) {
if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) {
- powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), null);
+ powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), stAb);
}
}
if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
@@ -1646,7 +1639,7 @@ public class AttachAi extends SpellAbilityAi {
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
} else if (keyword.endsWith("CARDNAME attacks each turn if able.") || keyword.endsWith("CARDNAME attacks each combat if able.")) {
return ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true) && !ai.getCreaturesInPlay().isEmpty();
- } else if (keyword.endsWith("CARDNAME can't block.") || keyword.contains("CantBlock")) {
+ } else if (keyword.endsWith("CARDNAME can't block.")) {
return CombatUtil.canBlock(card, true);
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
for (SpellAbility ability : card.getSpellAbilities()) {
@@ -1699,7 +1692,7 @@ public class AttachAi extends SpellAbilityAi {
if (!c.getController().equals(ai)) {
return false;
}
- return c.getType().hasCreatureType(type);
+ return c.isValid(type, ai, sa.getHostCard(), sa);
}
});
List oppNonType = CardLists.filter(list, new Predicate() {
@@ -1709,7 +1702,7 @@ public class AttachAi extends SpellAbilityAi {
if (c.getController().equals(ai)) {
return false;
}
- return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c);
+ return !c.isValid(type, ai, sa.getHostCard(), sa) && !ComputerUtilCard.isUselessCreature(ai, c);
}
});
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
index e8a935cfe4c..95313e06a49 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
@@ -165,7 +165,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
}
}
- if (isHidden(sa)) {
+ if (sa.isHidden()) {
return hiddenOriginCanPlayAI(aiPlayer, sa);
}
return knownOriginCanPlayAI(aiPlayer, sa);
@@ -182,21 +182,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
- if (isHidden(sa)) {
+ if (sa.isHidden()) {
return hiddenOriginPlayDrawbackAI(aiPlayer, sa);
}
return knownOriginPlayDrawbackAI(aiPlayer, sa);
}
-
- private static boolean isHidden(SpellAbility sa) {
- boolean hidden = sa.hasParam("Hidden");
- if (!hidden && sa.hasParam("Origin")) {
- hidden = ZoneType.isHidden(sa.getParam("Origin"));
- }
- return hidden;
- }
-
/**
*
* changeZoneTriggerAINoCost.
@@ -232,7 +223,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return delta <= 0;
}
- if (isHidden(sa)) {
+ if (sa.isHidden()) {
return hiddenTriggerAI(aiPlayer, sa, mandatory);
}
return knownOriginTriggerAI(aiPlayer, sa, mandatory);
@@ -788,7 +779,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
}
- if (isHidden(sa)) {
+ if (sa.isHidden()) {
return true;
}
@@ -1316,8 +1307,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
private static Card canBouncePermanent(final Player ai, SpellAbility sa, CardCollectionView list) {
Game game = ai.getGame();
// filter out untargetables
- CardCollectionView aiPermanents = CardLists
- .filterControlledBy(list, ai);
+ CardCollectionView aiPermanents = CardLists.filterControlledBy(list, ai);
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
// Felidar Guardian + Saheeli Rai combo support
@@ -1870,23 +1860,25 @@ public class ChangeZoneAi extends SpellAbilityAi {
}));
}
- // JAVA 1.8 use Map.Entry.comparingByValue() somehow
- Map.Entry> max = Collections.max(data.entrySet(), new Comparator>>() {
- @Override
- public int compare(Map.Entry> o1, Map.Entry> o2) {
- return o1.getValue().getValue() - o2.getValue().getValue();
+ if (!data.isEmpty()) {
+ // JAVA 1.8 use Map.Entry.comparingByValue() somehow
+ Map.Entry> max = Collections.max(data.entrySet(), new Comparator>>() {
+ @Override
+ public int compare(Map.Entry> o1, Map.Entry> o2) {
+ return o1.getValue().getValue() - o2.getValue().getValue();
+ }
+ });
+
+ // filter list again by the opponent and a creature of the wanted name that can be targeted
+ list = CardLists.filter(CardLists.filterControlledBy(list, max.getKey()),
+ CardPredicates.nameEquals(max.getValue().getKey()), CardPredicates.isTargetableBy(sa));
+
+ // list should contain only one element or zero
+ sa.resetTargets();
+ for (Card c : list) {
+ sa.getTargets().add(c);
+ return true;
}
- });
-
- // filter list again by the opponent and a creature of the wanted name that can be targeted
- list = CardLists.filter(CardLists.filterControlledBy(list, max.getKey()),
- CardPredicates.nameEquals(max.getValue().getKey()), CardPredicates.isTargetableBy(sa));
-
- // list should contain only one element or zero
- sa.resetTargets();
- for (Card c : list) {
- sa.getTargets().add(c);
- return true;
}
return false;
@@ -1987,8 +1979,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
- // TODO use ComputerUtilCost.getMaxXValue if able
- toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
+ toPay = ComputerUtilCost.getMaxXValue(sa, ai);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java
index 46da4dca610..777776f5aa7 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java
@@ -227,7 +227,7 @@ public class CharmAi extends SpellAbilityAi {
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub);
if (chosenList.size() == min) {
- break; // enough choices
+ break; // enough choices
}
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
index 87bc3c09880..ec12e5ebf3a 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
@@ -52,8 +52,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
} else if ("GideonBlackblade".equals(aiLogic)) {
return SpecialCardAi.GideonBlackblade.consider(ai, sa);
- } else if ("SoulEcho".equals(aiLogic)) {
- return doTriggerAINoCost(ai, sa, true);
} else if ("Always".equals(aiLogic)) {
return true;
}
@@ -70,7 +68,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
- return checkApiLogic(aiPlayer, sa);
+ return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
}
@Override
diff --git a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java
index d6234051612..1eac12c1226 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java
@@ -56,7 +56,7 @@ public class CloneAi extends SpellAbilityAi {
bFlag |= (!c.isCreature() && !c.isTapped() && !(c.getTurnInZone() == phase.getTurn()));
// for creatures that could be improved (like Figure of Destiny)
- if (c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
+ if (c.isCreature() && (!sa.hasParam("Duration") || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
@@ -72,8 +72,7 @@ public class CloneAi extends SpellAbilityAi {
}
- if (!bFlag) { // All of the defined stuff is cloned, not very
- // useful
+ if (!bFlag) { // All of the defined stuff is cloned, not very useful
return false;
}
} else {
@@ -246,7 +245,7 @@ public class CloneAi extends SpellAbilityAi {
// Combat_Begin step
if (!ph.is(PhaseType.COMBAT_BEGIN)
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
- && !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
+ && !sa.hasParam("ActivationPhases") && sa.hasParam("Duration")) {
return false;
}
@@ -257,6 +256,6 @@ public class CloneAi extends SpellAbilityAi {
}
// don't activate during main2 unless this effect is permanent
- return !ph.is(PhaseType.MAIN2) || sa.hasParam("Permanent");
+ return !ph.is(PhaseType.MAIN2) || !sa.hasParam("Duration");
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java
index 65f11c69b35..78bdef0ceee 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java
@@ -31,8 +31,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
CardCollection list =
CardLists.getValidCards(AiAttackController.choosePreferredDefenderPlayer(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
- // AI won't try to grab cards that are filtered out of AI decks on
- // purpose
+ // AI won't try to grab cards that are filtered out of AI decks on purpose
list = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java
index f2d757d5872..5358f6e670d 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java
@@ -110,8 +110,7 @@ public class ControlGainAi extends SpellAbilityAi {
}
}
- // Don't steal something if I can't Attack without, or prevent it from
- // blocking at least
+ // Don't steal something if I can't Attack without, or prevent it from blocking at least
if (lose.contains("EOT")
&& ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !sa.isTrigger()) {
@@ -211,6 +210,7 @@ public class ControlGainAi extends SpellAbilityAi {
}
}
+ // TODO check life of controller and consider stealing from another opponent so the risk of your army disappearing is spread out
while (t == null) {
// filter by MustTarget requirement
CardCollection originalList = new CardCollection(list);
@@ -264,7 +264,6 @@ public class ControlGainAi extends SpellAbilityAi {
}
return true;
-
}
@Override
diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
index 764d74822be..37366e9c008 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
@@ -113,8 +113,7 @@ public class CounterAi extends SpellAbilityAi {
boolean setPayX = false;
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
- // TODO use ComputerUtilCost.getMaxXValue
- toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
+ toPay = Math.min(ComputerUtilCost.getMaxXValue(sa, ai), usableManaSources + 1);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
}
@@ -124,8 +123,7 @@ public class CounterAi extends SpellAbilityAi {
}
if (toPay <= usableManaSources) {
- // If this is a reusable Resource, feel free to play it most of
- // the time
+ // If this is a reusable Resource, feel free to play it most of the time
if (!SpellAbilityAi.playReusable(ai,sa)) {
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java
index 05bea452b91..37222ecc21d 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java
@@ -120,8 +120,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
- // pre filter targetable cards with counters and can receive one of
- // them
+ // pre filter targetable cards with counters and can receive one of them
list = CardLists.filter(list, new Predicate() {
@Override
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
index 75c5344095d..c3b3c130950 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
@@ -866,17 +866,16 @@ public class CountersPutAi extends SpellAbilityAi {
choice = Aggregates.random(list);
}
}
- if (choice != null && divided) {
- int alloc = Math.max(amount / totalTargets, 1);
- if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
- sa.addDividedAllocation(choice, left);
- } else {
- sa.addDividedAllocation(choice, alloc);
- left -= alloc;
- }
+ }
+ if (choice != null && divided) {
+ int alloc = Math.max(amount / totalTargets, 1);
+ if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
+ sa.addDividedAllocation(choice, left);
+ } else {
+ sa.addDividedAllocation(choice, alloc);
+ left -= alloc;
}
}
-
if (choice != null) {
sa.getTargets().add(choice);
list.remove(choice);
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java b/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java
index 3cd8a909a2b..c21c297fe62 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java
@@ -111,8 +111,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
final CardCollectionView hand = comp.getCardsIn(ZoneType.Hand);
if ((enemy.getLife() - restDamage) < 5) {
- // drop the human to less than 5
- // life
+ // drop the human to less than 5 life
return true;
}
@@ -147,12 +146,12 @@ public abstract class DamageAiBase extends SpellAbilityAi {
}
}
}
- if (value > 0) { //more likely to burn with larger hand
+ if (value > 0) { //more likely to burn with larger hand
for (int i = 3; i < hand.size(); i++) {
value *= 1.1f;
}
}
- if (value < 0.2f) { //hard floor to reduce ridiculous odds for instants over time
+ if (value < 0.2f) { //hard floor to reduce ridiculous odds for instants over time
return false;
} else {
final float chance = MyRandom.getRandom().nextFloat();
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
index 06884107ade..1c3cd26a61e 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
@@ -30,7 +30,6 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
-import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
@@ -41,7 +40,6 @@ import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
-import forge.game.player.PlayerPredicates;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
@@ -132,7 +130,7 @@ public class DamageDealAi extends DamageAiBase {
// Set PayX here to maximum value. It will be adjusted later depending on the target.
sa.setXManaCostPaid(dmg);
} else if (sa.getSVar(damage).contains("InYourHand") && source.isInZone(ZoneType.Hand)) {
- dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less
+ dmg = AbilityUtils.calculateAmount(source, damage, sa) - 1; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
if (sa.getTargetRestrictions().canTgtPlayer()) {
@@ -734,12 +732,10 @@ public class DamageDealAi extends DamageAiBase {
}
// When giving priority to targeting Creatures for mandatory
- // triggers
- // feel free to add the Human after we run out of good targets
+ // triggers feel free to add the Human after we run out of good targets
// TODO: add check here if card is about to die from something
- // on the stack
- // or from taking combat damage
+ // on the stack or from taking combat damage
final Cost abCost = sa.getPayCosts();
boolean freePing = immediately || abCost == null
@@ -796,8 +792,7 @@ public class DamageDealAi extends DamageAiBase {
}
}
}
- // TODO: Improve Damage, we shouldn't just target the player just
- // because we can
+ // TODO: Improve Damage, we shouldn't just target the player just because we can
if (sa.canTarget(enemy) && tcs.size() < tgt.getMaxTargets(source, sa)) {
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
@@ -985,7 +980,6 @@ public class DamageDealAi extends DamageAiBase {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
-
final Card source = sa.getHostCard();
final String damage = sa.getParam("NumDmg");
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
@@ -1042,7 +1036,7 @@ public class DamageDealAi extends DamageAiBase {
saTgt = saTgt.getParent();
}
- Player opponent = ai.getOpponents().min(PlayerPredicates.compareByLife());
+ Player opponent = ai.getWeakestOpponent();
// TODO: somehow account for the possible cost reduction?
int dmg = ComputerUtilMana.determineLeftoverMana(sa, ai, saTgt.getParam("XColor"));
@@ -1085,7 +1079,7 @@ public class DamageDealAi extends DamageAiBase {
saTgt.resetTargets();
saTgt.getTargets().add(tgtCreature != null && dmg < opponent.getLife() ? tgtCreature : opponent);
- sa.setXManaCostPaid(dmg);
+ saTgt.setXManaCostPaid(dmg);
return true;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DigAi.java b/forge-ai/src/main/java/forge/ai/ability/DigAi.java
index 33e3c315dc8..7ea8b772b97 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DigAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DigAi.java
@@ -45,9 +45,8 @@ public class DigAi extends SpellAbilityAi {
sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) {
return false;
- } else {
- sa.getTargets().add(opp);
}
+ sa.getTargets().add(opp);
libraryOwner = opp;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java
index 1e3b0350408..76cd8df5362 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java
@@ -28,9 +28,8 @@ public class DigMultipleAi extends SpellAbilityAi {
sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) {
return false;
- } else {
- sa.getTargets().add(opp);
}
+ sa.getTargets().add(opp);
libraryOwner = opp;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java
index d421a67f54d..35aa8d58a38 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java
@@ -60,9 +60,8 @@ public class DigUntilAi extends SpellAbilityAi {
sa.resetTargets();
if (!sa.canTarget(opp)) {
return false;
- } else {
- sa.getTargets().add(opp);
}
+ sa.getTargets().add(opp);
libraryOwner = opp;
} else {
if (sa.hasParam("Valid")) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
index 47d95a3698b..722de88e01d 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
@@ -257,7 +257,7 @@ public class DrawAi extends SpellAbilityAi {
} else {
numCards = ComputerUtilCost.getMaxXValue(sa, ai);
// try not to overdraw
- int safeDraw = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
+ int safeDraw = Math.abs(Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3));
if (sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) { safeDraw++; } // card will be spent
numCards = Math.min(numCards, safeDraw);
@@ -377,7 +377,7 @@ public class DrawAi extends SpellAbilityAi {
// checks what the ai prevent from casting it on itself
// if spell is not mandatory
if (aiTarget && !ai.cantLose()) {
- if (numCards >= computerLibrarySize) {
+ if (numCards >= computerLibrarySize - 3) {
if (xPaid) {
numCards = computerLibrarySize - 1;
if (numCards <= 0 && !mandatory) {
@@ -422,8 +422,7 @@ public class DrawAi extends SpellAbilityAi {
}
root.setXManaCostPaid(numCards);
} else {
- // Don't draw too many cards and then risk discarding
- // cards at EOT
+ // Don't draw too many cards and then risk discarding cards at EOT
if (!drawback && !mandatory) {
return false;
}
@@ -441,7 +440,7 @@ public class DrawAi extends SpellAbilityAi {
continue;
}
- // use xPaid abilties only for itself
+ // use xPaid abilities only for itself
if (xPaid) {
continue;
}
@@ -493,7 +492,7 @@ public class DrawAi extends SpellAbilityAi {
// TODO: consider if human is the defined player
// ability is not targeted
- if (numCards >= computerLibrarySize) {
+ if (numCards >= computerLibrarySize - 3) {
if (ai.isCardInPlay("Laboratory Maniac")) {
return true;
}
@@ -509,8 +508,7 @@ public class DrawAi extends SpellAbilityAi {
&& game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()
&& !assumeSafeX) {
- // Don't draw too many cards and then risk discarding cards at
- // EOT
+ // Don't draw too many cards and then risk discarding cards at EOT
if (!drawback) {
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
index 8614c4eb2d2..e643da47300 100644
--- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
@@ -79,7 +79,7 @@ public class EffectAi extends SpellAbilityAi {
if (!game.getStack().isEmpty()) {
return false;
}
- if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
+ if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return false;
}
if (!ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/FogAi.java b/forge-ai/src/main/java/forge/ai/ability/FogAi.java
index 6dc63330369..2fe66864e3f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/FogAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/FogAi.java
@@ -29,7 +29,7 @@ public class FogAi extends SpellAbilityAi {
final Card hostCard = sa.getHostCard();
// Don't cast it, if the effect is already in place
- if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
+ if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java b/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java
index 8bdd71b1394..198c1cde15c 100644
--- a/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java
@@ -13,8 +13,7 @@ public class GameLossAi extends SpellAbilityAi {
return false;
}
- // Only one SA Lose the Game card right now, which is Door to
- // Nothingness
+ // Only one SA Lose the Game card right now, which is Door to Nothingness
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
@@ -29,19 +28,23 @@ public class GameLossAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
+ Player loser = ai;
+
// Phage the Untouchable
// (Final Fortune would need to attach it's delayed trigger to a
// specific turn, which can't be done yet)
- Player opp = ai.getGame().getCombat().getDefenderPlayerByAttacker(sa.getHostCard());
+ if (ai.getGame().getCombat() != null) {
+ loser = ai.getGame().getCombat().getDefenderPlayerByAttacker(sa.getHostCard());
+ }
- if (!mandatory && opp.cantLose()) {
+ if (!mandatory && loser.cantLose()) {
return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
- sa.getTargets().add(opp);
+ sa.getTargets().add(loser);
}
return true;
diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java
index f00d8b18d31..8e95006b596 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java
@@ -266,8 +266,7 @@ public class LifeGainAi extends SpellAbilityAi {
}
if (!hasTgt && mandatory) {
// need to target something but its neither negative against
- // opponents,
- // nor posive against allies
+ // opponents, nor positive against allies
// hurting ally is probably better than healing opponent
// look for Lifegain not Negative (case of lifegain negated)
@@ -295,8 +294,7 @@ public class LifeGainAi extends SpellAbilityAi {
sa.getTargets().add(ally);
hasTgt = true;
}
- // better heal opponent which most life then the one with the
- // lowest
+ // better heal opponent which most life then the one with the lowest
if (!hasTgt) {
Player opp = opps.max(PlayerPredicates.compareByLife());
sa.getTargets().add(opp);
diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
index 2437c97a49a..df3cecd4588 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
@@ -28,7 +28,6 @@ public class LifeLoseAi extends SpellAbilityAi {
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
-
final PlayerCollection tgtPlayers = getPlayers(ai, sa);
final Card source = sa.getHostCard();
@@ -60,7 +59,6 @@ public class LifeLoseAi extends SpellAbilityAi {
return true;
}
-
/*
* (non-Javadoc)
*
@@ -123,12 +121,12 @@ public class LifeLoseAi extends SpellAbilityAi {
return false;
}
- final PlayerCollection tgtPlayers = getPlayers(ai, sa);
-
if (ComputerUtil.playImmediately(ai, sa)) {
return true;
}
+ final PlayerCollection tgtPlayers = getPlayers(ai, sa);
+ // TODO: check against the amount we could obtain when multiple activations are possible
PlayerCollection filteredPlayer = tgtPlayers
.filter(Predicates.and(PlayerPredicates.isOpponentOf(ai), PlayerPredicates.lifeLessOrEqualTo(amount)));
// killing opponents asap
diff --git a/forge-ai/src/main/java/forge/ai/ability/MillAi.java b/forge-ai/src/main/java/forge/ai/ability/MillAi.java
index f2216b0a5f8..520a54b63df 100644
--- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java
@@ -21,6 +21,7 @@ import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
+import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
@@ -214,13 +215,7 @@ public class MillAi extends SpellAbilityAi {
}
// get targeted or defined Player with largest library
- // TODO in Java 8 find better way
- final Player m = Collections.max(list, new Comparator() {
- @Override
- public int compare(Player arg0, Player arg1) {
- return arg0.getCardsIn(ZoneType.Library).size() - arg1.getCardsIn(ZoneType.Library).size();
- }
- });
+ final Player m = Collections.max(list, PlayerPredicates.compareByZoneSize(ZoneType.Library));
int cardsToDiscard = m.getCardsIn(ZoneType.Library).size();
diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java
index 1775616561a..e755393b75b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java
@@ -297,7 +297,7 @@ public class PermanentAi extends SpellAbilityAi {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false;
}
- return checkApiLogic(ai, sa);
+ return mandatory || checkApiLogic(ai, sa);
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java
index bd7db42f43f..69fc5f3b324 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java
@@ -8,7 +8,6 @@ import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
-import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
/**
@@ -40,10 +39,8 @@ public class PermanentNoncreatureAi extends PermanentAi {
if (host.hasSVar("OblivionRing")) {
SpellAbility effectExile = AbilityFactory.getAbility(host.getSVar("TrigExile"), host);
final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0);
- final TargetRestrictions tgt = effectExile.getTargetRestrictions();
- final CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, host,
- effectExile);
- CardCollection targets = CardLists.getTargetableCards(list, sa);
+ effectExile.setActivatingPlayer(ai);
+ CardCollection targets = CardLists.getTargetableCards(game.getCardsIn(origin), effectExile);
if (sourceName.equals("Suspension Field")
|| sourceName.equals("Detention Sphere")) {
// existing "exile until leaves" enchantments only target
diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
index 324794ea276..0fa0805fe7e 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
@@ -28,7 +28,6 @@ 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;
@@ -158,16 +157,7 @@ public class PlayAi extends SpellAbilityAi {
return true;
}
-
- /* (non-Javadoc)
- * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
- */
- @Override
- public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
- // as called from PlayEffect:173
- return true;
- }
-
+
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
index b77e246d183..02e582b8e69 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
@@ -400,7 +400,6 @@ public class PumpAi extends PumpAiBase {
if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
return true;
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
-
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)
|| game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) {
@@ -438,7 +437,7 @@ public class PumpAi extends PumpAiBase {
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !(sa.isCurse() && defense < 0)
&& !containsNonCombatKeyword(keywords)
- && !sa.hasParam("UntilYourNextTurn")
+ && !"UntilYourNextTurn".equals(sa.getParam("Duration"))
&& !"Snapcaster".equals(sa.getParam("AILogic"))
&& !isFight) {
return false;
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
index 0fc65c64997..5e708dbdd9f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
@@ -44,7 +44,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
-
public boolean grantsUsefulExtraBlockOpts(final Player ai, final SpellAbility sa, final Card card, List keywords) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
@@ -110,7 +109,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
&& (card.getNetCombatDamage() > 0)
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
} else if (keyword.endsWith("CARDNAME can't attack or block.")) {
- if (sa.hasParam("UntilYourNextTurn")) {
+ if ("UntilYourNextTurn".equals(sa.getParam("Duration"))) {
return CombatUtil.canAttack(card, ai) || CombatUtil.canBlock(card, true);
}
if (!ph.isPlayerTurn(ai)) {
@@ -154,14 +153,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
}
});
return CombatUtil.canBlockAtLeastOne(card, attackers);
- } else if (keyword.endsWith("CantBlockCardUIDSource")) { // can't block CARDNAME this turn
- if (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
- || ph.getPhase().isBefore(PhaseType.MAIN1) || !CombatUtil.canBlock(sa.getHostCard(), card)) {
- return false;
- }
- // target needs to be a creature, controlled by the player which is attacked
- return !sa.getHostCard().isTapped() || (combat != null && combat.isAttacking(sa.getHostCard())
- && card.getController().equals(combat.getDefenderPlayerByAttacker(sa.getHostCard())));
} else if (keyword.endsWith("This card doesn't untap during your next untap step.")) {
return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai)
&& Untap.canUntap(card);
@@ -480,7 +471,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
}
}); // leaves all creatures that will be destroyed
} // -X/-X end
- else if (attack < 0 && !game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
+ else if (attack < 0 && !game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
// spells that give -X/0
boolean isMyTurn = game.getPhaseHandler().isPlayerTurn(ai);
if (isMyTurn) {
@@ -514,7 +505,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
else {
final boolean addsKeywords = !keywords.isEmpty();
if (addsKeywords) {
-
// If the keyword can prevent a creature from attacking, see if there's some kind of viable prioritization
if (keywords.contains("CARDNAME can't attack.") || keywords.contains("CARDNAME can't attack or block.")
|| keywords.contains("HIDDEN CARDNAME can't attack.") || keywords.contains("HIDDEN CARDNAME can't attack or block.")) {
@@ -540,8 +530,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
protected boolean containsNonCombatKeyword(final List keywords) {
for (final String keyword : keywords) {
- // since most keywords are combat relevant check for those that are
- // not
+ // since most keywords are combat relevant check for those that are not
if (keyword.endsWith("This card doesn't untap during your next untap step.")
|| keyword.endsWith("Shroud") || keyword.endsWith("Hexproof")) {
return true;
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
index 386d043770d..cb68ba3a711 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
@@ -113,7 +113,7 @@ public class PumpAllAi extends PumpAiBase {
if (phase.isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| phase.isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())
- || game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
+ || game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
return false;
}
int totalPower = 0;
@@ -151,6 +151,12 @@ public class PumpAllAi extends PumpAiBase {
return true;
}
+ @Override
+ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
+ // important to call canPlay first so targets are added if needed
+ return canPlayAI(ai, sa) || mandatory;
+ }
+
boolean pumpAgainstRemoval(Player ai, SpellAbility sa, List comp) {
final List objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
for (final Card c : comp) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java b/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java
index b5b9ece8fc5..79edfb3f416 100644
--- a/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java
@@ -49,14 +49,6 @@ import forge.game.zone.ZoneType;
*/
public class RegenerateAi extends SpellAbilityAi {
- // Ex: A:SP$Regenerate | Cost$W | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$Regenerate
- // target creature.
- // http://www.slightlymagic.net/wiki/Forge_AbilityFactory#Regenerate
-
- // **************************************************************
- // ********************* Regenerate ****************************
- // **************************************************************
-
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
@@ -65,8 +57,7 @@ public class RegenerateAi extends SpellAbilityAi {
boolean chance = false;
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
- // As far as I can tell these Defined Cards will only have one of
- // them
+ // As far as I can tell these Defined Cards will only have one of them
final List list = AbilityUtils.getDefinedCards(hostCard, sa.getParam("Defined"), sa);
if (!game.getStack().isEmpty()) {
@@ -105,8 +96,7 @@ public class RegenerateAi extends SpellAbilityAi {
}
if (!game.getStack().isEmpty()) {
- // check stack for something on the stack will kill anything i
- // control
+ // check stack for something on the stack will kill anything i control
final List objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
final List threatenedTargets = new ArrayList<>();
@@ -191,8 +181,7 @@ public class RegenerateAi extends SpellAbilityAi {
}
}
- // TODO see if something on the stack is about to kill something i
- // can target
+ // TODO see if something on the stack is about to kill something i can target
// choose my best X without regen
if (CardLists.getNotType(compTargetables, "Creature").isEmpty()) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java
index 310ea94efdf..3ec32296b5b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java
@@ -18,7 +18,7 @@ public class RepeatAi extends SpellAbilityAi {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.usesTargeting()) {
- if (!opp.canBeTargetedBy(sa)) {
+ if (!sa.canTarget(opp)) {
return false;
}
sa.resetTargets();
diff --git a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
index 063daaddd34..9fd83435a03 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
@@ -119,11 +119,9 @@ public class ScryAi extends SpellAbilityAi {
return false;
}
- double chance = .4; // 40 percent chance of milling with instant speed
- // stuff
+ double chance = .4; // 40 percent chance of milling with instant speed stuff
if (SpellAbilityAi.isSorcerySpeed(sa)) {
- chance = .667; // 66.7% chance for sorcery speed (since it will
- // never activate EOT)
+ chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
diff --git a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
index 98b8e1e16c2..29ded831c7f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
@@ -52,8 +52,7 @@ public class SetStateAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
- // Gross generalization, but this always considers alternate
- // states more powerful
+ // Gross generalization, but this always considers alternate states more powerful
return !sa.getHostCard().isInAlternateState();
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java b/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java
index f9c3301ed7f..e4101790445 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java
@@ -17,8 +17,7 @@ public class ShuffleAi extends SpellAbilityAi {
return aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2, aiPlayer);
}
- // not really sure when the compy would use this; maybe only after a
- // human
+ // not really sure when the compy would use this; maybe only after a human
// deliberately put a card on top of their library
return false;
/*
diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java
index a8f46f2a180..95efebb90c8 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java
@@ -11,6 +11,7 @@ import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
+import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -309,9 +310,8 @@ public abstract class TapAiBase extends SpellAbilityAi {
return true;
}
- // TODO: use Defined to determine, if this is an unfavorable result
-
- return true;
+ final List pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
+ return pDefined.isEmpty() || (pDefined.get(0).isUntapped() && pDefined.get(0).getController() != ai);
} else {
sa.resetTargets();
if (tapPrefTargeting(ai, source, sa, mandatory)) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java
index ff456311037..214cbbc4763 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java
@@ -12,7 +12,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
-import forge.game.card.CardPredicates.Presets;
+import forge.game.card.CardPredicates;
import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -49,7 +49,7 @@ public class TapAllAi extends SpellAbilityAi {
}
validTappables = CardLists.getValidCards(validTappables, valid, source.getController(), source, sa);
- validTappables = CardLists.filter(validTappables, Presets.UNTAPPED);
+ validTappables = CardLists.filter(validTappables, CardPredicates.Presets.UNTAPPED);
if (sa.hasParam("AILogic")) {
String logic = sa.getParam("AILogic");
@@ -69,18 +69,8 @@ public class TapAllAi extends SpellAbilityAi {
return false;
}
- final List human = CardLists.filter(validTappables, new Predicate() {
- @Override
- public boolean apply(final Card c) {
- return c.getController().equals(opp);
- }
- });
- final List compy = CardLists.filter(validTappables, new Predicate() {
- @Override
- public boolean apply(final Card c) {
- return c.getController().equals(ai);
- }
- });
+ final List human = CardLists.filterControlledBy(validTappables, opp);
+ final List compy = CardLists.filterControlledBy(validTappables, ai);
if (human.size() <= compy.size()) {
return false;
}
@@ -102,7 +92,7 @@ public class TapAllAi extends SpellAbilityAi {
final Game game = source.getGame();
CardCollectionView tmpList = game.getCardsIn(ZoneType.Battlefield);
tmpList = CardLists.getValidCards(tmpList, valid, source.getController(), source, sa);
- tmpList = CardLists.filter(tmpList, Presets.UNTAPPED);
+ tmpList = CardLists.filter(tmpList, CardPredicates.Presets.UNTAPPED);
return tmpList;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java
index 949bb217141..e052b4737c9 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java
@@ -19,8 +19,7 @@ public class TapOrUntapAi extends TapAiBase {
if (!sa.usesTargeting()) {
// assume we are looking to tap human's stuff
- // TODO - check for things with untap abilities, and don't tap
- // those.
+ // TODO - check for things with untap abilities, and don't tap those.
boolean bFlag = false;
for (final Card c : AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)) {
@@ -40,6 +39,4 @@ public class TapOrUntapAi extends TapAiBase {
return randomReturn;
}
-
-
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
index 47c443f59b8..8816b0a5506 100644
--- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
@@ -66,7 +66,7 @@ public class UntapAi extends SpellAbilityAi {
if (!sa.usesTargeting()) {
final List pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
- return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai;
+ return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai);
} else {
return untapPrefTargeting(ai, sa, false);
}
@@ -82,9 +82,8 @@ public class UntapAi extends SpellAbilityAi {
return false;
}
- // TODO: use Defined to determine, if this is an unfavorable result
final List pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
- return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai;
+ return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai);
} else {
if (untapPrefTargeting(ai, sa, mandatory)) {
return true;
diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java
index a0166598ed1..7c53560db5c 100644
--- a/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java
@@ -8,6 +8,7 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
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.zone.ZoneType;
@@ -29,6 +30,10 @@ public class UntapAllAi extends SpellAbilityAi {
valid = sa.getParam("ValidCards");
}
list = CardLists.getValidCards(list, valid.split(","), source.getController(), source, sa);
+ // don't untap if only opponent benefits
+ PlayerCollection goodControllers = aiPlayer.getAllies();
+ goodControllers.add(aiPlayer);
+ list = CardLists.filter(list, CardPredicates.isControlledByAnyOf(goodControllers));
return !list.isEmpty();
}
return false;
diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
index eccdeb48f16..3e9d300fe7b 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
@@ -88,7 +88,6 @@ public class GameCopier {
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn());
newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn());
- newPlayer.setPreventNextDamage(origPlayer.getPreventNextDamage());
newPlayer.getManaPool().add(origPlayer.getManaPool());
newPlayer.setCommanders(origPlayer.getCommanders()); // will be fixed up below
playerMap.put(origPlayer, newPlayer);
diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java
index c769e5055ab..ac7c162207b 100644
--- a/forge-core/src/main/java/forge/StaticData.java
+++ b/forge-core/src/main/java/forge/StaticData.java
@@ -151,6 +151,11 @@ public class StaticData {
return this.editions;
}
+ public final CardEdition.Collection getCustomEditions(){
+ return this.customEditions;
+ }
+
+
private List sortedEditions;
public final List getSortedEditions() {
if (sortedEditions == null) {
@@ -158,12 +163,24 @@ public class StaticData {
for (CardEdition set : editions) {
sortedEditions.add(set);
}
+ if (customEditions.size() > 0){
+ for (CardEdition set : customEditions) {
+ sortedEditions.add(set);
+ }
+ }
Collections.sort(sortedEditions);
Collections.reverse(sortedEditions); //put newer sets at the top
}
return sortedEditions;
}
+ public CardEdition getCardEdition(String setCode){
+ CardEdition edition = this.editions.get(setCode);
+ if (edition == null) // try custom editions
+ edition = this.customEditions.get(setCode);
+ return edition;
+ }
+
public PaperCard getOrLoadCommonCard(String cardName, String setCode, int artIndex, boolean foil) {
PaperCard card = commonCards.getCard(cardName, setCode, artIndex);
boolean isCustom = false;
diff --git a/forge-core/src/main/java/forge/card/CardAiHints.java b/forge-core/src/main/java/forge/card/CardAiHints.java
index a7b2c8e7255..746f7297a4f 100644
--- a/forge-core/src/main/java/forge/card/CardAiHints.java
+++ b/forge-core/src/main/java/forge/card/CardAiHints.java
@@ -6,7 +6,6 @@ package forge.card;
*/
public class CardAiHints {
-
private final boolean isRemovedFromAIDecks;
private final boolean isRemovedFromRandomDecks;
private final boolean isRemovedFromNonCommanderDecks;
@@ -15,7 +14,6 @@ public class CardAiHints {
private final DeckHints deckNeeds;
private final DeckHints deckHas;
-
public CardAiHints(boolean remAi, boolean remRandom, boolean remUnlessCommander, DeckHints dh, DeckHints dn, DeckHints has) {
isRemovedFromAIDecks = remAi;
isRemovedFromRandomDecks = remRandom;
@@ -90,5 +88,4 @@ public class CardAiHints {
}
}
-
}
diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java
index 0e274d60d00..366be7e81f8 100644
--- a/forge-core/src/main/java/forge/card/CardDb.java
+++ b/forge-core/src/main/java/forge/card/CardDb.java
@@ -533,6 +533,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
public int getPrintCount(String cardName, String edition) {
int cnt = 0;
+ if (edition == null || cardName == null)
+ return cnt;
for (PaperCard pc : getAllCards(cardName)) {
if (pc.getEdition().equals(edition)) {
cnt++;
@@ -544,6 +546,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
public int getMaxPrintCount(String cardName) {
int max = -1;
+ if (cardName == null)
+ return max;
for (PaperCard pc : getAllCards(cardName)) {
if (max < pc.getArtIndex()) {
max = pc.getArtIndex();
@@ -555,6 +559,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
public int getArtCount(String cardName, String setName) {
int cnt = 0;
+ if (cardName == null || setName == null)
+ return cnt;
Collection cards = getAllCards(cardName);
if (null == cards) {
@@ -628,6 +634,23 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}));
}
+ public Collection getAllNonPromosNonReprintsNoAlt() {
+ return Lists.newArrayList(Iterables.filter(getAllCardsNoAlt(), new Predicate() {
+ @Override
+ public boolean apply(final PaperCard paperCard) {
+ CardEdition edition = null;
+ try {
+ edition = editions.getEditionByCodeOrThrow(paperCard.getEdition());
+ if (edition.getType() == Type.PROMOS||edition.getType() == Type.REPRINT)
+ return false;
+ } catch (Exception ex) {
+ return false;
+ }
+ return true;
+ }
+ }));
+ }
+
public String getName(final String cardName) {
if (alternateName.containsKey(cardName)) {
return alternateName.get(cardName);
@@ -749,7 +772,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
public PaperCard createUnsupportedCard(String cardName) {
-
CardRequest request = CardRequest.fromString(cardName);
CardEdition cardEdition = CardEdition.UNKNOWN;
CardRarity cardRarity = CardRarity.Unknown;
@@ -790,7 +812,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1);
-
}
private final Editor editor = new Editor();
diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java
index 04d41cdfb89..e5b5e233a95 100644
--- a/forge-core/src/main/java/forge/card/CardEdition.java
+++ b/forge-core/src/main/java/forge/card/CardEdition.java
@@ -19,7 +19,6 @@ package forge.card;
import java.io.File;
import java.io.FilenameFilter;
-import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -121,13 +120,16 @@ public final class CardEdition implements Comparable { // immutable
// commonly used printsheets with collector number
public enum EditionSectionWithCollectorNumbers {
CARDS("cards"),
+ SPECIAL_SLOT("special slot"), //to help with convoluted boosters
PRECON_PRODUCT("precon product"),
BORDERLESS("borderless"),
SHOWCASE("showcase"),
EXTENDED_ART("extended art"),
ALTERNATE_ART("alternate art"),
+ ALTERNATE_FRAME("alternate frame"),
BUY_A_BOX("buy a box"),
PROMO("promo"),
+ BUNDLE("bundle"),
BOX_TOPPER("box topper");
private final String name;
@@ -148,7 +150,7 @@ public final class CardEdition implements Comparable { // immutable
}
}
- public static class CardInSet {
+ public static class CardInSet implements Comparable {
public final CardRarity rarity;
public final String collectorNumber;
public final String name;
@@ -172,6 +174,56 @@ public final class CardEdition implements Comparable { // immutable
sb.append(name);
return sb.toString();
}
+
+ /**
+ * This method implements the main strategy to allow for natural ordering of collectorNumber
+ * (i.e. "1" < "10"), overloading the default lexicographic order (i.e. "10" < "1").
+ * Any non-numerical parts in the input collectorNumber will be also accounted for, and attached to the
+ * resulting sorting key, accordingly.
+ *
+ * @param collectorNumber: Input collectorNumber tro transform in a Sorting Key
+ * @return A 5-digits zero-padded collector number + any non-numerical parts attached.
+ */
+ public static String getSortableCollectorNumber(final String collectorNumber){
+ String sortableCollNr = collectorNumber;
+ if (sortableCollNr == null || sortableCollNr.length() == 0)
+ sortableCollNr = "50000"; // very big number of 5 digits to have them in last positions
+
+ // Now, for proper sorting, let's zero-pad the collector number (if integer)
+ int collNr;
+ try {
+ collNr = Integer.parseInt(sortableCollNr);
+ sortableCollNr = String.format("%05d", collNr);
+ } catch (NumberFormatException ex) {
+ String nonNumeric = sortableCollNr.replaceAll("[0-9]", "");
+ String onlyNumeric = sortableCollNr.replaceAll("[^0-9]", "");
+ try {
+ collNr = Integer.parseInt(onlyNumeric);
+ } catch (NumberFormatException exon) {
+ collNr = 0; // this is the case of ONLY-letters collector numbers
+ }
+ if ((collNr > 0) && (sortableCollNr.startsWith(onlyNumeric))) // e.g. 12a, 37+, 2018f,
+ sortableCollNr = String.format("%05d", collNr) + nonNumeric;
+ else // e.g. WS6, S1
+ sortableCollNr = nonNumeric + String.format("%05d", collNr);
+ }
+ return sortableCollNr;
+ }
+
+ @Override
+ public int compareTo(CardInSet o) {
+ final int nameCmp = name.compareToIgnoreCase(o.name);
+ if (0 != nameCmp) {
+ return nameCmp;
+ }
+ String thisCollNr = getSortableCollectorNumber(collectorNumber);
+ String othrCollNr = getSortableCollectorNumber(o.collectorNumber);
+ final int collNrCmp = thisCollNr.compareTo(othrCollNr);
+ if (0 != collNrCmp) {
+ return collNrCmp;
+ }
+ return rarity.compareTo(o.rarity);
+ }
}
private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
@@ -206,6 +258,7 @@ public final class CardEdition implements Comparable { // immutable
private String[] chaosDraftThemes = new String[0];
private final ListMultimap cardMap;
+ private final List cardsInSet;
private final Map tokenNormalized;
// custom print sheets that will be loaded lazily
private final Map> customPrintSheetsToParse;
@@ -215,13 +268,18 @@ public final class CardEdition implements Comparable { // immutable
private CardEdition(ListMultimap cardMap, Map tokens, Map> customPrintSheetsToParse) {
this.cardMap = cardMap;
+ this.cardsInSet = new ArrayList<>(cardMap.values());
+ Collections.sort(cardsInSet);
this.tokenNormalized = tokens;
this.customPrintSheetsToParse = customPrintSheetsToParse;
}
private CardEdition(CardInSet[] cards, Map tokens) {
+ List cardsList = Arrays.asList(cards);
this.cardMap = ArrayListMultimap.create();
- this.cardMap.replaceValues("cards", Arrays.asList(cards));
+ this.cardMap.replaceValues("cards", cardsList);
+ this.cardsInSet = new ArrayList<>(cardsList);
+ Collections.sort(cardsInSet);
this.tokenNormalized = tokens;
this.customPrintSheetsToParse = new HashMap<>();
}
@@ -256,7 +314,7 @@ public final class CardEdition implements Comparable { // immutable
date = date + "-01";
try {
return formatter.parse(date);
- } catch (ParseException e) {
+ } catch (Exception e) {
return new Date();
}
}
@@ -287,7 +345,7 @@ public final class CardEdition implements Comparable { // immutable
public List getCards() { return cardMap.get("cards"); }
public List getAllCardsInSet() {
- return Lists.newArrayList(cardMap.values());
+ return cardsInSet;
}
public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others
@@ -306,7 +364,10 @@ public final class CardEdition implements Comparable { // immutable
if (o == null) {
return 1;
}
- return date.compareTo(o.date);
+ int dateComp = date.compareTo(o.date);
+ if (0 != dateComp)
+ return dateComp;
+ return name.compareTo(o.name);
}
@Override
@@ -340,7 +401,7 @@ public final class CardEdition implements Comparable { // immutable
}
public boolean isLargeSet() {
- return getAllCardsInSet().size() > 200 && !smallSetOverride;
+ return this.cardsInSet.size() > 200 && !smallSetOverride;
}
public int getCntBoosterPictures() {
@@ -414,7 +475,7 @@ public final class CardEdition implements Comparable { // immutable
* rarity - grouping #4
* name - grouping #5
*/
- "(^([0-9]+.?) )?(([SCURML]) )?(.*)$"
+ "(^([0-9A-Z]+.?) )?(([SCURML]) )?(.*)$"
);
ListMultimap cardMap = ArrayListMultimap.create();
diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java
index 4e80fe8bc1a..0aac2fd8695 100644
--- a/forge-core/src/main/java/forge/card/CardRules.java
+++ b/forge-core/src/main/java/forge/card/CardRules.java
@@ -84,6 +84,12 @@ public final class CardRules implements ICardCharacteristics {
boolean isReminder = false;
boolean isSymbol = false;
String oracleText = face.getOracleText();
+ // CR 903.4 colors defined by its characteristic-defining abilities
+ for (String staticAbility : face.getStaticAbilities()) {
+ if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("SetColor$ All")) {
+ res |= MagicColor.ALL_COLORS;
+ }
+ }
int len = oracleText.length();
for(int i = 0; i < len; i++) {
char c = oracleText.charAt(i); // This is to avoid needless allocations performed by toCharArray()
@@ -388,7 +394,6 @@ public final class CardRules implements ICardCharacteristics {
this.removedFromNonCommanderDecks = "NonCommander".equalsIgnoreCase(value);
}
} else if ("AlternateMode".equals(key)) {
- //System.out.println(faces[curFace].getName());
this.altMode = CardSplitType.smartValueOf(value);
} else if ("ALTERNATE".equals(key)) {
this.curFace = 1;
@@ -539,7 +544,6 @@ public final class CardRules implements ICardCharacteristics {
@Override
public final ManaCostShard next() {
final String unparsed = st.nextToken();
- // System.out.println(unparsed);
if (StringUtils.isNumeric(unparsed)) {
this.genericCost += Integer.parseInt(unparsed);
return null;
@@ -555,7 +559,7 @@ public final class CardRules implements ICardCharacteristics {
*/
@Override
public void remove() {
- } // unsuported
+ } // unsupported
}
}
diff --git a/forge-core/src/main/java/forge/card/mana/ManaCostParser.java b/forge-core/src/main/java/forge/card/mana/ManaCostParser.java
index 925e4ceae1b..cd635219665 100644
--- a/forge-core/src/main/java/forge/card/mana/ManaCostParser.java
+++ b/forge-core/src/main/java/forge/card/mana/ManaCostParser.java
@@ -30,7 +30,6 @@ public class ManaCostParser implements IParserManaCost {
*/
public ManaCostParser(final String cost) {
this.cost = cost.split(" ");
- // System.out.println(cost);
this.nextToken = 0;
this.genericCost = 0;
}
@@ -66,7 +65,6 @@ public class ManaCostParser implements IParserManaCost {
@Override
public final ManaCostShard next() {
final String unparsed = this.cost[this.nextToken++];
- // System.out.println(unparsed);
if (StringUtils.isNumeric(unparsed)) {
this.genericCost += Integer.parseInt(unparsed);
return null;
diff --git a/forge-core/src/main/java/forge/item/IPaperCard.java b/forge-core/src/main/java/forge/item/IPaperCard.java
index 198c4b8f4b0..bb2101b862b 100644
--- a/forge-core/src/main/java/forge/item/IPaperCard.java
+++ b/forge-core/src/main/java/forge/item/IPaperCard.java
@@ -17,10 +17,11 @@ import forge.card.MagicColor;
import forge.util.PredicateCard;
import forge.util.PredicateString;
-//import forge.Card;
-
public interface IPaperCard extends InventoryItem, Serializable {
+ String NO_COLLECTOR_NUMBER = "N.A."; // Placeholder for No-Collection number available
+ int DEFAULT_ART_INDEX = 1;
+
/**
* Number of filters based on CardPrinted values.
*/
@@ -228,6 +229,7 @@ public interface IPaperCard extends InventoryItem, Serializable {
String getName();
String getEdition();
+ String getCollectorNumber();
int getArtIndex();
boolean isFoil();
boolean isToken();
diff --git a/forge-core/src/main/java/forge/item/PaperCard.java b/forge-core/src/main/java/forge/item/PaperCard.java
index 58fb032c898..216fddb3852 100644
--- a/forge-core/src/main/java/forge/item/PaperCard.java
+++ b/forge-core/src/main/java/forge/item/PaperCard.java
@@ -6,12 +6,12 @@
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
- *
+ *
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
@@ -26,6 +26,7 @@ import com.google.common.base.Function;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.CardDb;
+import forge.card.CardEdition;
import forge.card.CardRarity;
import forge.card.CardRules;
import forge.util.CardTranslation;
@@ -36,7 +37,7 @@ import forge.util.TextUtil;
* A lightweight version of a card that matches real-world cards, to use outside of games (eg. inventory, decks, trade).
*
* The full set of rules is in the CardRules class.
- *
+ *
* @author Forge
*/
public final class PaperCard implements Comparable, InventoryItemFromSet, IPaperCard, Serializable {
@@ -48,12 +49,19 @@ public final class PaperCard implements Comparable, InventoryItemFro
// These fields are kinda PK for PrintedCard
private final String name;
private final String edition;
+ /* [NEW] Attribute to store reference to CollectorNumber of each PaperCard.
+ By default the attribute is marked as "unset" so that it could be retrieved and set.
+ (see getCollectorNumber())
+ */
+ private String collectorNumber = null;
private final int artIndex;
private final boolean foil;
private Boolean hasImage;
// Calculated fields are below:
private transient CardRarity rarity; // rarity is given in ctor when set is assigned
+ // Reference to a new instance of Self, but foiled!
+ private transient PaperCard foiledVersion = null;
@Override
public String getName() {
@@ -65,6 +73,23 @@ public final class PaperCard implements Comparable, InventoryItemFro
return edition;
}
+ @Override
+ public String getCollectorNumber() {
+ /* The collectorNumber attribute is managed in a property-like fashion.
+ By default it is marked as "unset" (-1), which integrates with all constructors
+ invocations not including this as an extra parameter. In this way, the new
+ attribute could be added to the API with minimum disruption to code
+ throughout the other packages.
+ If "unset", the corresponding collectorNumber will be retrieved
+ from the corresponding CardEdition (see retrieveCollectorNumber)
+ * */
+ if (collectorNumber == null) {
+ collectorNumber = this.retrieveCollectorNumber();
+ }
+
+ return collectorNumber;
+ }
+
@Override
public int getArtIndex() {
return artIndex;
@@ -90,6 +115,20 @@ public final class PaperCard implements Comparable, InventoryItemFro
return rarity;
}
+ /* FIXME: At the moment, every card can get Foiled, with no restriction on the
+ corresponding Edition - so we could Foil even Alpha cards.
+ */
+ public PaperCard getFoiled() {
+ if (this.foil)
+ return this;
+
+ if (this.foiledVersion == null) {
+ this.foiledVersion = new PaperCard(this.rules, this.edition, this.rarity,
+ this.artIndex, true, String.valueOf(collectorNumber));
+ }
+ return this.foiledVersion;
+ }
+
// @Override
// public String getImageKey() {
// return getImageLocator(getImageName(), getArtIndex(), true, false);
@@ -124,11 +163,16 @@ public final class PaperCard implements Comparable, InventoryItemFro
}
};
+ public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0) {
+ this(rules0, edition0, rarity0, IPaperCard.DEFAULT_ART_INDEX, false);
+ }
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0) {
this(rules0, edition0, rarity0, artIndex0, false);
}
- public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0, final boolean foil0) {
+
+ public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0,
+ final boolean foil0) {
if (rules0 == null || edition0 == null || rarity0 == null) {
throw new IllegalArgumentException("Cannot create card without rules, edition or rarity");
}
@@ -140,6 +184,17 @@ public final class PaperCard implements Comparable, InventoryItemFro
rarity = rarity0;
}
+ public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0,
+ final String collectorNumber0) {
+ this(rules0, edition0, rarity0, artIndex0, false, collectorNumber0);
+ }
+
+ public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
+ final int artIndex0, final boolean foil0, final String collectorNumber0) {
+ this(rules0, edition0, rarity0, artIndex0, foil0);
+ collectorNumber = collectorNumber0;
+ }
+
// Want this class to be a key for HashTable
@Override
public boolean equals(final Object obj) {
@@ -160,32 +215,27 @@ public final class PaperCard implements Comparable, InventoryItemFro
if (!edition.equals(other.edition)) {
return false;
}
- if ((other.foil != foil) || (other.artIndex != artIndex)) {
+ if (!getCollectorNumber().equals(other.getCollectorNumber()))
return false;
- }
-
- return true;
+ return (other.foil == foil) && (other.artIndex == artIndex);
}
/*
* (non-Javadoc)
- *
+ *
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
- final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) + (artIndex * 2);
+ final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) +
+ (artIndex * 2) + (getCollectorNumber().hashCode() * 383);
if (foil) {
return code + 1;
}
return code;
}
- /*
- * (non-Javadoc)
- *
- * @see java.lang.Object#toString()
- */
+ // FIXME: Check
@Override
public String toString() {
return CardTranslation.getTranslatedName(name);
@@ -194,21 +244,49 @@ public final class PaperCard implements Comparable, InventoryItemFro
}
/*
- * (non-Javadoc)
- *
- * @see java.lang.Comparable#compareTo(java.lang.Object)
+ * This (utility) method transform a collectorNumber String into a key string for sorting.
+ * This method proxies the same strategy implemented in CardEdition.CardInSet class from which the
+ * collectorNumber of PaperCard instances are originally retrieved.
+ * This is also to centralise the criterion, whilst avoiding code duplication.
+ *
+ * Note: The method has been made private as this is for internal API use **only**, to allow
+ * for generalised comparison with IPaperCard instances (see compareTo)
+ *
+ * The public API of PaperCard includes a method (i.e. getCollectorNumberSortingKey) which applies
+ * this method on instance's own collector number.
+ *
+ * @return a zero-padded 5-digits String + any non-numerical content in the input String, properly attached.
*/
+ private static String makeCollectorNumberSortingKey(final String collectorNumber0){
+ String collectorNumber = collectorNumber0;
+ if (collectorNumber.equals(NO_COLLECTOR_NUMBER))
+ collectorNumber = null;
+ return CardEdition.CardInSet.getSortableCollectorNumber(collectorNumber);
+ }
+
+ public String getCollectorNumberSortingKey(){
+ // Hardly the case, but just invoke getter rather than direct
+ // attribute to be sure that collectorNumber has been retrieved already!
+ return makeCollectorNumberSortingKey(getCollectorNumber());
+ }
+
+
@Override
public int compareTo(final IPaperCard o) {
- final int nameCmp = getName().compareToIgnoreCase(o.getName());
+ final int nameCmp = name.compareToIgnoreCase(o.getName());
if (0 != nameCmp) {
return nameCmp;
}
- // TODO compare sets properly
+ //FIXME: compare sets properly
int setDiff = edition.compareTo(o.getEdition());
- if ( 0 != setDiff )
+ if (0 != setDiff)
return setDiff;
-
+ String thisCollNrKey = getCollectorNumberSortingKey();
+ String othrCollNrKey = makeCollectorNumberSortingKey(o.getCollectorNumber());
+ final int collNrCmp = thisCollNrKey.compareTo(othrCollNrKey);
+ if (0 != collNrCmp) {
+ return collNrCmp;
+ }
return Integer.compare(artIndex, o.getArtIndex());
}
@@ -216,8 +294,7 @@ public final class PaperCard implements Comparable, InventoryItemFro
// default deserialization
ois.defaultReadObject();
- IPaperCard pc = null;
- pc = StaticData.instance().getCommonCards().getCard(name, edition, artIndex);
+ IPaperCard pc = StaticData.instance().getCommonCards().getCard(name, edition, artIndex);
if (pc == null) {
pc = StaticData.instance().getVariantCards().getCard(name, edition, artIndex);
if (pc == null) {
@@ -228,9 +305,35 @@ public final class PaperCard implements Comparable, InventoryItemFro
rarity = pc.getRarity();
}
+ private String retrieveCollectorNumber() {
+ StaticData data = StaticData.instance();
+ CardEdition edition = data.getEditions().get(this.edition);
+ if (edition == null) {
+ edition = data.getCustomEditions().get(this.edition);
+ if (edition == null) // don't bother continuing - non-existing card!
+ return NO_COLLECTOR_NUMBER;
+ }
+ int artIndexCount = 0;
+ String collectorNumberInEdition = "";
+ for (CardEdition.CardInSet card : edition.getAllCardsInSet()) {
+ if (card.name.equalsIgnoreCase(this.name)) {
+ artIndexCount += 1;
+ if (artIndexCount == this.artIndex) {
+ collectorNumberInEdition = card.collectorNumber;
+ break;
+ }
+ }
+ }
+ // CardEdition stores collectorNumber as a String, which is null if there isn't any.
+ // In this case, the NO_COLLECTOR_NUMBER value (i.e. 0) is returned.
+ return ((collectorNumberInEdition != null) && (collectorNumberInEdition.length() > 0)) ?
+ collectorNumberInEdition : NO_COLLECTOR_NUMBER;
+ }
+
@Override
public String getImageKey(boolean altState) {
- String imageKey = ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator + edition + CardDb.NameSetSeparator + artIndex;
+ String imageKey = ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator
+ + edition + CardDb.NameSetSeparator + artIndex;
if (altState) {
imageKey += ImageKeys.BACKFACE_POSTFIX;
}
diff --git a/forge-core/src/main/java/forge/item/PaperToken.java b/forge-core/src/main/java/forge/item/PaperToken.java
index 74fc8c4d707..776d17ceed8 100644
--- a/forge-core/src/main/java/forge/item/PaperToken.java
+++ b/forge-core/src/main/java/forge/item/PaperToken.java
@@ -132,6 +132,12 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override public String toString() { return name; }
@Override public String getEdition() { return edition != null ? edition.getCode() : "???"; }
+
+ @Override
+ public String getCollectorNumber() {
+ return IPaperCard.NO_COLLECTOR_NUMBER;
+ }
+
@Override public int getArtIndex() { return artIndex; }
@Override public boolean isFoil() { return false; }
@Override public CardRules getRules() { return card; }
diff --git a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java
index 33e3fa8eadd..d4301cf2667 100644
--- a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java
+++ b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java
@@ -70,12 +70,14 @@ public class BoosterGenerator {
}
private static PaperCard generateFoilCard(PrintSheet sheet) {
- return StaticData.instance().getCommonCards().getFoiled(sheet.random(1, true).get(0));
+ PaperCard randomCard = sheet.random(1, true).get(0);
+ return randomCard.getFoiled();
}
private static PaperCard generateFoilCard(List cardList) {
Collections.shuffle(cardList, MyRandom.getRandom());
- return StaticData.instance().getCommonCards().getFoiled(cardList.get(0));
+ PaperCard randomCard = cardList.get(0);
+ return randomCard.getFoiled();
}
public static List getBoosterPack(SealedProduct.Template template) {
@@ -461,7 +463,7 @@ public class BoosterGenerator {
if (toReplace != null) {
// Keep the foil state
if (toReplace.isFoil()) {
- toAdd = StaticData.instance().getCommonCards().getFoiled(toAdd);
+ toAdd = toAdd.getFoiled();
}
booster.remove(toReplace);
booster.add(toAdd);
diff --git a/forge-core/src/main/java/forge/util/FileUtil.java b/forge-core/src/main/java/forge/util/FileUtil.java
index d12ef2741ff..64cbdfedf1b 100644
--- a/forge-core/src/main/java/forge/util/FileUtil.java
+++ b/forge-core/src/main/java/forge/util/FileUtil.java
@@ -21,7 +21,6 @@ import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
-import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -208,7 +207,7 @@ public final class FileUtil {
if ((file == null) || !file.exists()) {
return new ArrayList<>();
}
- return FileUtil.readAllLines(new FileReader(file), false);
+ return FileUtil.readAllLines(file, false);
} catch (final Exception ex) {
throw new RuntimeException("FileUtil : readFile() error, " + ex);
}
@@ -248,6 +247,31 @@ public final class FileUtil {
}
return list;
}
+ /**
+ * Reads all lines from given file to a list of strings.
+ *
+ * @param file is the File to read.
+ * @param mayTrim defines whether to trim lines.
+ * @return list of strings
+ */
+ public static List readAllLines(final File file, final boolean mayTrim) {
+ final List list = new ArrayList<>();
+ try {
+ final BufferedReader in = new BufferedReader(
+ new InputStreamReader(new FileInputStream(file), "UTF-8"));
+ String line;
+ while ((line = in.readLine()) != null) {
+ if (mayTrim) {
+ line = line.trim();
+ }
+ list.add(line);
+ }
+ in.close();
+ } catch (final IOException ex) {
+ throw new RuntimeException("FileUtil : readAllLines() error, " + ex);
+ }
+ return list;
+ }
// returns a list of pairs. if the name is not in the file, it is synthesized from the url
public static List> readNameUrlFile(String nameUrlFile) {
diff --git a/forge-core/src/main/java/forge/util/ImageUtil.java b/forge-core/src/main/java/forge/util/ImageUtil.java
index 3ff22655c5e..63dae0114e3 100644
--- a/forge-core/src/main/java/forge/util/ImageUtil.java
+++ b/forge-core/src/main/java/forge/util/ImageUtil.java
@@ -99,7 +99,7 @@ public class ImageUtil {
final CardDb db = StaticData.instance().getCommonCards();
return db.getRules(card.getMeldWith()).getOtherPart().getName();
} else {
- return null;
+ return null;
}
else
return null;
@@ -118,6 +118,27 @@ public class ImageUtil {
return getImageRelativePath(cp, backFace, true, true);
}
+ public static String getScryfallDownloadUrl(PaperCard cp, boolean backFace, String setCode){
+ return getScryfallDownloadUrl(cp, backFace, setCode, "en");
+ }
+
+ public static String getScryfallDownloadUrl(PaperCard cp, boolean backFace, String setCode, String langCode){
+ String editionCode;
+ if ((setCode != null) && (setCode.length() > 0))
+ editionCode = setCode;
+ else
+ editionCode = cp.getEdition().toLowerCase();
+ String cardCollectorNumber = cp.getCollectorNumber();
+ // Hack to account for variations in Arabian Nights
+ cardCollectorNumber = cardCollectorNumber.replace("+", "†");
+ String faceParam = "";
+ if (cp.getRules().getOtherPart() != null) {
+ faceParam = (backFace ? "&face=back" : "&face=front");
+ }
+ return String.format("%s/%s/%s?format=image&version=normal%s", editionCode, cardCollectorNumber,
+ langCode, faceParam);
+ }
+
public static String toMWSFilename(String in) {
final StringBuilder out = new StringBuilder();
char c;
@@ -130,4 +151,4 @@ public class ImageUtil {
}
return out.toString();
}
-}
+}
\ No newline at end of file
diff --git a/forge-core/src/main/java/forge/util/Localizer.java b/forge-core/src/main/java/forge/util/Localizer.java
index 60acb7e0b59..741d3c7c79b 100644
--- a/forge-core/src/main/java/forge/util/Localizer.java
+++ b/forge-core/src/main/java/forge/util/Localizer.java
@@ -15,147 +15,155 @@ import java.util.MissingResourceException;
import java.util.ResourceBundle;
public class Localizer {
-
- private static Localizer instance;
- private List observers = new ArrayList<>();
-
- private Locale locale;
- private ResourceBundle resourceBundle;
+ private static Localizer instance;
- public static Localizer getInstance() {
- if (instance == null) {
- synchronized (Localizer.class) {
- instance = new Localizer();
- }
- }
- return instance;
- }
-
- private Localizer() {
- }
-
- public void initialize(String localeID, String languagesDirectory) {
- setLanguage(localeID, languagesDirectory);
- }
+ private List observers = new ArrayList<>();
- public String convert(String value, String fromEncoding, String toEncoding) throws UnsupportedEncodingException {
- return new String(value.getBytes(fromEncoding), toEncoding);
- }
+ private Locale locale;
+ private ResourceBundle resourceBundle;
- public String charset(String value, String charsets[]) {
- String probe = StandardCharsets.UTF_8.name();
- for(String c : charsets) {
- Charset charset = Charset.forName(c);
- if(charset != null) {
- try {
- if (value.equals(convert(convert(value, charset.name(), probe), probe, charset.name()))) {
- return c;
- }
- } catch(UnsupportedEncodingException ignored) {}
- }
- }
- return StandardCharsets.UTF_8.name();
- }
+ public static Localizer getInstance() {
+ if (instance == null) {
+ synchronized (Localizer.class) {
+ instance = new Localizer();
+ }
+ }
+ return instance;
+ }
- public String getMessage(final String key, final Object... messageArguments) {
- MessageFormat formatter = null;
+ private Localizer() {
+ }
- try {
- //formatter = new MessageFormat(resourceBundle.getString(key.toLowerCase()), locale);
- formatter = new MessageFormat(resourceBundle.getString(key), locale);
- } catch (final IllegalArgumentException | MissingResourceException e) {
- e.printStackTrace();
- }
-
- if (formatter == null) {
- System.err.println("INVALID PROPERTY: '" + key + "' -- Translation Needed?");
- return "INVALID PROPERTY: '" + key + "' -- Translation Needed?";
- }
-
- formatter.setLocale(locale);
-
- String formattedMessage = "CHAR ENCODING ERROR";
- final String[] charsets = { "ISO-8859-1", "UTF-8" };
- //Support non-English-standard characters
- String detectedCharset = charset(resourceBundle.getString(key), charsets);
+ public void initialize(String localeID, String languagesDirectory) {
+ setLanguage(localeID, languagesDirectory);
+ }
- final int argLength = messageArguments.length;
- Object[] syncEncodingMessageArguments = new Object[argLength];
- //when messageArguments encoding not equal resourceBundle.getString(key),convert to equal
- //avoid convert to a have two encoding content formattedMessage string.
- for (int i = 0; i < argLength; i++) {
- String objCharset = charset(messageArguments[i].toString(), charsets);
- try {
- syncEncodingMessageArguments[i] = convert(messageArguments[i].toString(), objCharset, detectedCharset);
- } catch (UnsupportedEncodingException ignored) {
- System.err.println("Cannot Convert '" + messageArguments[i].toString() + "' from '" + objCharset + "' To '" + detectedCharset + "'");
- return "encoding '" + key + "' translate string failure";
- }
- }
+ public String convert(String value, String fromEncoding, String toEncoding) throws UnsupportedEncodingException {
+ return new String(value.getBytes(fromEncoding), toEncoding);
+ }
- try {
- formattedMessage = new String(formatter.format(syncEncodingMessageArguments).getBytes(detectedCharset), StandardCharsets.UTF_8);
- } catch(UnsupportedEncodingException ignored) {}
+ public String charset(String value, String charsets[]) {
+ String probe = StandardCharsets.UTF_8.name();
+ for(String c : charsets) {
+ Charset charset = Charset.forName(c);
+ if(charset != null) {
+ try {
+ if (value.equals(convert(convert(value, charset.name(), probe), probe, charset.name()))) {
+ return c;
+ }
+ } catch(UnsupportedEncodingException ignored) {}
+ }
+ }
+ return StandardCharsets.UTF_8.name();
+ }
- return formattedMessage;
- }
+ public String getMessageorUseDefault(final String key, final String defaultValue, final Object... messageArguments) {
+ try {
+ return getMessage(key, messageArguments);
+ } catch (Exception e) {
+ return defaultValue;
+ }
+ }
+ //FIXME: localizer should return default value from english locale or it will crash some GUI element like the NewGameMenu->NewGameScreen Popup when returned null...
+ public String getMessage(final String key, final Object... messageArguments) {
+ MessageFormat formatter = null;
- public void setLanguage(final String languageRegionID, final String languagesDirectory) {
-
- String[] splitLocale = languageRegionID.split("-");
-
- Locale oldLocale = locale;
- locale = new Locale(splitLocale[0], splitLocale[1]);
-
- //Don't reload the language if nothing changed
- if (oldLocale == null || !oldLocale.equals(locale)) {
+ try {
+ //formatter = new MessageFormat(resourceBundle.getString(key.toLowerCase()), locale);
+ formatter = new MessageFormat(resourceBundle.getString(key), locale);
+ } catch (final IllegalArgumentException | MissingResourceException e) {
+ e.printStackTrace();
+ }
- File file = new File(languagesDirectory);
- URL[] urls = null;
-
- try {
- urls = new URL[] { file.toURI().toURL() };
- } catch (MalformedURLException e) {
- e.printStackTrace();
- }
+ if (formatter == null) {
+ System.err.println("INVALID PROPERTY: '" + key + "' -- Translation Needed?");
+ return "INVALID PROPERTY: '" + key + "' -- Translation Needed?";
+ }
- ClassLoader loader = new URLClassLoader(urls);
+ formatter.setLocale(locale);
- try {
- resourceBundle = ResourceBundle.getBundle(languageRegionID, new Locale(splitLocale[0], splitLocale[1]), loader);
- } catch (NullPointerException | MissingResourceException e) {
- //If the language can't be loaded, default to US English
- resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader);
- e.printStackTrace();
- }
+ String formattedMessage = "CHAR ENCODING ERROR";
+ final String[] charsets = { "ISO-8859-1", "UTF-8" };
+ //Support non-English-standard characters
+ String detectedCharset = charset(resourceBundle.getString(key), charsets);
- System.out.println("Language '" + resourceBundle.toString() + "' loaded successfully.");
-
- notifyObservers();
-
- }
-
- }
-
- public List getLanguages() {
- //TODO List all languages by getting their files
- return null;
- }
-
- public void registerObserver(LocalizationChangeObserver observer) {
- observers.add(observer);
- }
-
- private void notifyObservers() {
- for (LocalizationChangeObserver observer : observers) {
- observer.localizationChanged();
- }
- }
+ final int argLength = messageArguments.length;
+ Object[] syncEncodingMessageArguments = new Object[argLength];
+ //when messageArguments encoding not equal resourceBundle.getString(key),convert to equal
+ //avoid convert to a have two encoding content formattedMessage string.
+ for (int i = 0; i < argLength; i++) {
+ String objCharset = charset(messageArguments[i].toString(), charsets);
+ try {
+ syncEncodingMessageArguments[i] = convert(messageArguments[i].toString(), objCharset, detectedCharset);
+ } catch (UnsupportedEncodingException ignored) {
+ System.err.println("Cannot Convert '" + messageArguments[i].toString() + "' from '" + objCharset + "' To '" + detectedCharset + "'");
+ return "encoding '" + key + "' translate string failure";
+ }
+ }
- public static class Language {
- public String languageName;
- public String languageID;
- }
+ try {
+ formattedMessage = new String(formatter.format(syncEncodingMessageArguments).getBytes(detectedCharset), StandardCharsets.UTF_8);
+ } catch(UnsupportedEncodingException ignored) {}
+
+ return formattedMessage;
+ }
+
+ public void setLanguage(final String languageRegionID, final String languagesDirectory) {
+
+ String[] splitLocale = languageRegionID.split("-");
+
+ Locale oldLocale = locale;
+ locale = new Locale(splitLocale[0], splitLocale[1]);
+
+ //Don't reload the language if nothing changed
+ if (oldLocale == null || !oldLocale.equals(locale)) {
+
+ File file = new File(languagesDirectory);
+ URL[] urls = null;
+
+ try {
+ urls = new URL[] { file.toURI().toURL() };
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+
+ ClassLoader loader = new URLClassLoader(urls);
+
+ try {
+ resourceBundle = ResourceBundle.getBundle(languageRegionID, new Locale(splitLocale[0], splitLocale[1]), loader);
+ } catch (NullPointerException | MissingResourceException e) {
+ //If the language can't be loaded, default to US English
+ resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader);
+ e.printStackTrace();
+ }
+
+ System.out.println("Language '" + resourceBundle.toString() + "' loaded successfully.");
+
+ notifyObservers();
+
+ }
+
+ }
+
+ public List getLanguages() {
+ //TODO List all languages by getting their files
+ return null;
+ }
+
+ public void registerObserver(LocalizationChangeObserver observer) {
+ observers.add(observer);
+ }
+
+ private void notifyObservers() {
+ for (LocalizationChangeObserver observer : observers) {
+ observer.localizationChanged();
+ }
+ }
+
+ public static class Language {
+ public String languageName;
+ public String languageID;
+ }
}
diff --git a/forge-game/src/main/java/forge/game/CardTraitBase.java b/forge-game/src/main/java/forge/game/CardTraitBase.java
index 78d4bacc2b7..b9db1d3fb06 100644
--- a/forge-game/src/main/java/forge/game/CardTraitBase.java
+++ b/forge-game/src/main/java/forge/game/CardTraitBase.java
@@ -162,6 +162,9 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
* @return a boolean.
*/
public boolean matchesValid(final Object o, final String[] valids, final Card srcCard) {
+ if (srcCard == null) {
+ return false;
+ }
return matchesValid(o, valids, srcCard, srcCard.getController());
}
@@ -188,13 +191,17 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
return matchesValid(o, valids, getHostCard());
}
- public boolean matchesValidParam(String param, final Object o) {
- if (hasParam(param) && !matchesValid(o, getParam(param).split(","))) {
+ public boolean matchesValidParam(String param, final Object o, final Card srcCard) {
+ if (hasParam(param) && !matchesValid(o, getParam(param).split(","), srcCard)) {
return false;
}
return true;
}
+ public boolean matchesValidParam(String param, final Object o) {
+ return matchesValidParam(param, o, getHostCard());
+ }
+
/**
* Sets the suppressed.
*
@@ -341,7 +348,6 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
}
list = CardLists.getValidCards(list, sIsPresent.split(","), this.getHostCard().getController(), this.getHostCard(), this);
-
final String rightString = presentCompare.substring(2);
int right = AbilityUtils.calculateAmount(getHostCard(), rightString, this);
final int left = list.size();
@@ -434,7 +440,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
}
if (params.containsKey("WerewolfTransformCondition")) {
- if (!CardUtil.getLastTurnCast("Card", this.getHostCard()).isEmpty()) {
+ if (!CardUtil.getLastTurnCast("Card", this.getHostCard(), this).isEmpty()) {
return false;
}
}
diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java
index cfd9b21e4f7..f322fd5ad39 100644
--- a/forge-game/src/main/java/forge/game/Game.java
+++ b/forge-game/src/main/java/forge/game/Game.java
@@ -756,7 +756,7 @@ public class Game {
// Rule 800.4 Losing a Multiplayer game
CardCollectionView cards = this.getCardsInGame();
boolean planarControllerLost = false;
- boolean isMultiplayer = this.getPlayers().size() > 2;
+ boolean isMultiplayer = getPlayers().size() > 2;
// 702.142f & 707.9
// If a player leaves the game, all face-down cards that player owns must be revealed to all players.
@@ -772,7 +772,7 @@ public class Game {
for (Card c : cards) {
// CR 800.4d if card is controlled by opponent, LTB should trigger
if (c.getOwner().equals(p) && c.getController().equals(p)) {
- c.getCurrentState().clearTriggers();
+ c.getGame().getTriggerHandler().clearActiveTriggers(c, null);
}
}
@@ -793,16 +793,16 @@ public class Game {
}
getAction().ceaseToExist(c, false);
// CR 603.2f owner of trigger source lost game
- triggerHandler.clearDelayedTrigger(c);
+ getTriggerHandler().clearDelayedTrigger(c);
} else {
// return stolen permanents
- if (c.getController().equals(p) && c.isInZone(ZoneType.Battlefield)) {
+ if ((c.getController().equals(p) || c.getZone().getPlayer().equals(p)) && c.isInZone(ZoneType.Battlefield)) {
c.removeTempController(p);
getAction().controllerChangeZoneCorrection(c);
}
c.removeTempController(p);
if (c.getController().equals(p)) {
- this.getAction().exile(c, null);
+ getAction().exile(c, null);
}
}
} else {
@@ -830,7 +830,7 @@ public class Game {
}
// Remove leftover items from
- this.getStack().removeInstancesControlledBy(p);
+ getStack().removeInstancesControlledBy(p);
getTriggerHandler().onPlayerLost(p);
@@ -1054,7 +1054,8 @@ public class Game {
public void onCleanupPhase() {
clearCounterAddedThisTurn();
- for (Player player : getPlayers()) {
+ // some cards need this info updated even after a player lost, so don't skip them
+ for (Player player : getRegisteredPlayers()) {
player.onCleanupPhase();
}
}
diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java
index 89ec43318e4..b916a1b721e 100644
--- a/forge-game/src/main/java/forge/game/GameAction.java
+++ b/forge-game/src/main/java/forge/game/GameAction.java
@@ -47,6 +47,7 @@ import forge.game.ability.effects.AttachEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
+import forge.game.card.CardDamageMap;
import forge.game.card.CardFactory;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
@@ -61,6 +62,7 @@ import forge.game.event.GameEventCardTapped;
import forge.game.event.GameEventFlipCoin;
import forge.game.event.GameEventGameStarted;
import forge.game.event.GameEventScry;
+import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.mulligan.MulliganService;
import forge.game.player.GameLossReason;
@@ -144,8 +146,8 @@ public class GameAction {
boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield);
boolean wasFacedown = c.isFaceDown();
- //Rule 110.5g: A token that has left the battlefield can't move to another zone
- if (c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Stack)) {
+ // Rule 111.8: A token that has left the battlefield can't move to another zone
+ if (!c.isSpell() && c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Stack)) {
return c;
}
@@ -241,7 +243,7 @@ public class GameAction {
copied = CardFactory.copyCard(c, false);
if (zoneTo.is(ZoneType.Stack)) {
- // when moving to stack, copy changed card infomation
+ // when moving to stack, copy changed card information
copied.setChangedCardColors(c.getChangedCardColors());
copied.setChangedCardKeywords(c.getChangedCardKeywords());
copied.setChangedCardTypes(c.getChangedCardTypesMap());
@@ -342,6 +344,7 @@ public class GameAction {
c.updateStateForView();
}
}
+
return c;
}
}
@@ -371,7 +374,7 @@ public class GameAction {
if (saTargeting != null) {
saTargeting.getTargets().replaceTargetCard(c, cards);
}
- // Replace host rememberd cards
+ // Replace host remembered cards
// But not replace RememberLKI, since it wants to refer to the last known info.
Card hostCard = cause.getHostCard();
if (!cause.hasParam("RememberLKI") && hostCard.isRemembered(c)) {
@@ -438,7 +441,7 @@ public class GameAction {
}
if (mergedCards != null) {
- // Move components of merged permanet here
+ // Move components of merged permanent here
// Also handle 721.3e and 903.9a
boolean wasToken = c.isToken();
if (commanderEffect != null) {
@@ -938,7 +941,10 @@ public class GameAction {
game.getCombat().removeFromCombat(c);
game.getCombat().saveLKI(lki);
}
- game.getTriggerHandler().registerActiveLTBTrigger(lki);
+ // again, make sure no triggers run from cards leaving controlled by loser
+ if (!lki.getController().equals(lki.getOwner())) {
+ game.getTriggerHandler().registerActiveLTBTrigger(lki);
+ }
final Map runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.CardLKI, lki);
runParams.put(AbilityKey.Origin, c.getZone().getZoneType().name());
@@ -1352,8 +1358,7 @@ public class GameAction {
if (c.getCounters(CounterEnumType.LORE) < c.getFinalChapterNr()) {
return false;
}
- if (!game.getStack().hasSimultaneousStackEntries() &&
- !game.getStack().hasSourceOnStack(c, SpellAbilityPredicates.isChapter())) {
+ if (!game.getStack().hasSourceOnStack(c, SpellAbilityPredicates.isChapter())) {
sacrifice(c, null, table);
checkAgain = true;
}
@@ -1680,7 +1685,9 @@ public class GameAction {
final Map runParams = AbilityKey.mapFromCard(c);
runParams.put(AbilityKey.Causer, activator);
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);
return sacrificed != null;
}
@@ -1695,7 +1702,7 @@ public class GameAction {
}
final Card newCard = moveToGraveyard(c, cause, null);
- if (table != null) {
+ if (table != null && newCard != null && newCard.getZone() != null) {
table.put(ZoneType.Battlefield, newCard.getZone().getZoneType(), newCard);
}
@@ -2121,4 +2128,44 @@ public class GameAction {
}
}
}
+
+ public void dealDamage(final boolean isCombat, final CardDamageMap damageMap, final CardDamageMap preventMap,
+ final GameEntityCounterTable counterTable, final SpellAbility cause) {
+ // Clear assigned damage if is combat
+ for (Map.Entry> et : damageMap.columnMap().entrySet()) {
+ final GameEntity ge = et.getKey();
+ if (isCombat && ge instanceof Card) {
+ ((Card) ge).clearAssignedDamage();
+ }
+ }
+
+ // Run replacement effect for each entity dealt damage
+ game.getReplacementHandler().runReplaceDamage(isCombat, damageMap, preventMap, counterTable, cause);
+
+ // Actually deal damage according to replaced damage map
+ for (Map.Entry> et : damageMap.rowMap().entrySet()) {
+ final Card sourceLKI = et.getKey();
+ int sum = 0;
+ for (Map.Entry e : et.getValue().entrySet()) {
+ if (e.getValue() <= 0) {
+ continue;
+ }
+ sum += e.getValue();
+ e.getKey().addDamageAfterPrevention(e.getValue(), sourceLKI, isCombat, counterTable);
+ }
+
+ if (sourceLKI.hasKeyword(Keyword.LIFELINK)) {
+ sourceLKI.getController().gainLife(sum, sourceLKI, cause);
+ }
+ }
+
+ preventMap.triggerPreventDamage(isCombat);
+ preventMap.clear();
+
+ damageMap.triggerDamageDoneOnce(isCombat, game);
+ damageMap.clear();
+
+ counterTable.triggerCountersPutAll(game);
+ counterTable.clear();
+ }
}
diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java
index 285e3e15a0b..ba4a2f26498 100644
--- a/forge-game/src/main/java/forge/game/GameEntity.java
+++ b/forge-game/src/main/java/forge/game/GameEntity.java
@@ -23,11 +23,11 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
-import forge.game.ability.AbilityKey;
+import forge.game.ability.AbilityUtils;
+import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
-import forge.game.card.CardDamageMap;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
@@ -35,19 +35,17 @@ import forge.game.card.CounterType;
import forge.game.event.GameEventCardAttachment;
import forge.game.keyword.Keyword;
import forge.game.player.Player;
+import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
-import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
public abstract class GameEntity extends GameObject implements IIdentifiable {
protected final int id;
private String name = "";
- private int preventNextDamage = 0;
protected CardCollection attachedCards = new CardCollection();
- private Map> preventionShieldsWithEffects = Maps.newTreeMap();
protected Map counters = Maps.newHashMap();
protected GameEntity(int id0) {
@@ -67,216 +65,61 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
getView().updateName(this);
}
- public final int addDamage(final int damage, final Card source, boolean isCombat, boolean noPrevention,
- final CardDamageMap damageMap, final CardDamageMap preventMap, GameEntityCounterTable counterTable, final SpellAbility cause) {
- if (noPrevention) {
- return addDamageWithoutPrevention(damage, source, damageMap, preventMap, counterTable, cause);
- } else if (isCombat) {
- return addCombatDamage(damage, source, damageMap, preventMap, counterTable);
- } else {
- return addDamage(damage, source, damageMap, preventMap, counterTable, cause);
- }
- }
-
- public int addDamage(final int damage, final Card source, final CardDamageMap damageMap,
- final CardDamageMap preventMap, GameEntityCounterTable counterTable, final SpellAbility cause) {
- int damageToDo = damage;
-
- damageToDo = replaceDamage(damageToDo, source, false, true, damageMap, preventMap, counterTable, cause);
- damageToDo = preventDamage(damageToDo, source, false, preventMap, cause);
-
- return addDamageAfterPrevention(damageToDo, source, false, damageMap, counterTable);
- }
-
- public final int addCombatDamage(final int damage, final Card source, final CardDamageMap damageMap,
- final CardDamageMap preventMap, GameEntityCounterTable counterTable) {
- int damageToDo = damage;
-
- damageToDo = replaceDamage(damageToDo, source, true, true, damageMap, preventMap, counterTable, null);
- damageToDo = preventDamage(damageToDo, source, true, preventMap, null);
-
- if (damageToDo > 0) {
- source.getDamageHistory().registerCombatDamage(this);
- }
- // damage prevention is already checked
- return addCombatDamageBase(damageToDo, source, damageMap, counterTable);
- }
-
- protected int addCombatDamageBase(final int damage, final Card source, CardDamageMap damageMap, GameEntityCounterTable counterTable) {
- return addDamageAfterPrevention(damage, source, true, damageMap, counterTable);
- }
-
- public int addDamageWithoutPrevention(final int damage, final Card source, final CardDamageMap damageMap,
- final CardDamageMap preventMap, GameEntityCounterTable counterTable, final SpellAbility cause) {
- int damageToDo = replaceDamage(damage, source, false, false, damageMap, preventMap, counterTable, cause);
- return addDamageAfterPrevention(damageToDo, source, false, damageMap, counterTable);
- }
-
- public int replaceDamage(final int damage, final Card source, final boolean isCombat, final boolean prevention,
- final CardDamageMap damageMap, final CardDamageMap preventMap, GameEntityCounterTable counterTable, final SpellAbility cause) {
- // Replacement effects
- final Map repParams = AbilityKey.mapFromAffected(this);
- repParams.put(AbilityKey.DamageSource, source);
- repParams.put(AbilityKey.DamageAmount, damage);
- repParams.put(AbilityKey.IsCombat, isCombat);
- repParams.put(AbilityKey.NoPreventDamage, !prevention);
- repParams.put(AbilityKey.DamageMap, damageMap);
- repParams.put(AbilityKey.PreventMap, preventMap);
- repParams.put(AbilityKey.CounterTable, counterTable);
- if (cause != null) {
- repParams.put(AbilityKey.Cause, cause);
- }
-
- switch (getGame().getReplacementHandler().run(ReplacementType.DamageDone, repParams)) {
- case NotReplaced:
- return damage;
- case Updated:
- int newDamage = (int) repParams.get(AbilityKey.DamageAmount);
- GameEntity newTarget = (GameEntity) repParams.get(AbilityKey.Affected);
- // check if this is still the affected card or player
- if (this.equals(newTarget)) {
- return newDamage;
- } else {
- if (prevention) {
- newDamage = newTarget.preventDamage(newDamage, source, isCombat, preventMap, cause);
- }
- newTarget.addDamageAfterPrevention(newDamage, source, isCombat, damageMap, counterTable);
- }
- default:
- return 0;
- }
- }
-
// This function handles damage after replacement and prevention effects are applied
- public abstract int addDamageAfterPrevention(final int damage, final Card source, final boolean isCombat, CardDamageMap damageMap, GameEntityCounterTable counterTable);
+ public abstract int addDamageAfterPrevention(final int damage, final Card source, final boolean isCombat, GameEntityCounterTable counterTable);
// This should be also usable by the AI to forecast an effect (so it must
// not change the game state)
- public abstract int staticDamagePrevention(final int damage, final Card source, final boolean isCombat, final boolean isTest);
+ public int staticDamagePrevention(final int damage, final int possiblePrevention, final Card source, final boolean isCombat) {
+ if (damage <= 0) {
+ return 0;
+ }
+ if (!source.canDamagePrevented(isCombat)) {
+ return damage;
+ }
+
+ if (isCombat && getGame().getReplacementHandler().isPreventCombatDamageThisTurn()) {
+ return 0;
+ }
+
+ for (final Card ca : getGame().getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
+ for (final ReplacementEffect re : ca.getReplacementEffects()) {
+ if (!re.getMode().equals(ReplacementType.DamageDone) ||
+ (!re.hasParam("PreventionEffect") && !re.hasParam("Prevent"))) {
+ continue;
+ }
+ if (!re.matchesValidParam("ValidSource", source)) {
+ continue;
+ }
+ if (!re.matchesValidParam("ValidTarget", this)) {
+ continue;
+ }
+ if (re.hasParam("IsCombat")) {
+ if (re.getParam("IsCombat").equals("True") != isCombat) {
+ continue;
+ }
+ }
+ if (re.hasParam("Prevent")) {
+ return 0;
+ } else if (re.getOverridingAbility() != null) {
+ SpellAbility repSA = re.getOverridingAbility();
+ if (repSA.getApi() == ApiType.ReplaceDamage) {
+ return Math.max(0, damage - AbilityUtils.calculateAmount(ca, repSA.getParam("Amount"), repSA));
+ }
+ }
+ return 0;
+ }
+ }
+
+ return Math.max(0, damage - possiblePrevention);
+ }
// This should be also usable by the AI to forecast an effect (so it must
// not change the game state)
public abstract int staticReplaceDamage(final int damage, final Card source, final boolean isCombat);
- public final int preventDamage(
- final int damage, final Card source, final boolean isCombat, CardDamageMap preventMap,
- final SpellAbility cause) {
- if (!source.canDamagePrevented(isCombat)) {
- return damage;
- }
-
- int restDamage = damage;
-
- // first try to replace the damage
- final Map repParams = AbilityKey.mapFromAffected(this);
- repParams.put(AbilityKey.DamageSource, source);
- repParams.put(AbilityKey.DamageAmount, damage);
- repParams.put(AbilityKey.IsCombat, isCombat);
- repParams.put(AbilityKey.Prevention, true);
- repParams.put(AbilityKey.PreventMap, preventMap);
- if (cause != null) {
- repParams.put(AbilityKey.Cause, cause);
- }
-
- switch (getGame().getReplacementHandler().run(ReplacementType.DamageDone, repParams)) {
- case NotReplaced:
- restDamage = damage;
- break;
- case Updated:
- restDamage = (int) repParams.get(AbilityKey.DamageAmount);
- break;
- default:
- restDamage = 0;
- }
-
- // then apply static Damage Prevention effects
- restDamage = staticDamagePrevention(restDamage, source, isCombat, false);
-
- // then apply ShieldEffects with Special Effect
- restDamage = preventShieldEffect(restDamage);
-
- // then do Shield with only number
- if (restDamage <= 0) {
- restDamage = 0;
- } else if (restDamage >= getPreventNextDamage()) {
- restDamage = restDamage - getPreventNextDamage();
- setPreventNextDamage(0);
- } else {
- setPreventNextDamage(getPreventNextDamage() - restDamage);
- restDamage = 0;
- }
-
- // if damage is greater than restDamage, damage was prevented
- if (damage > restDamage) {
- int prevent = damage - restDamage;
- preventMap.put(source, this, damage - restDamage);
-
- final Map runParams = AbilityKey.newMap();
- runParams.put(AbilityKey.DamageTarget, this);
- runParams.put(AbilityKey.DamageAmount, prevent);
- runParams.put(AbilityKey.DamageSource, source);
- runParams.put(AbilityKey.IsCombatDamage, isCombat);
-
- getGame().getTriggerHandler().runTrigger(TriggerType.DamagePrevented, runParams, false);
- }
-
- return restDamage;
- }
-
- protected abstract int preventShieldEffect(final int damage);
-
- public int getPreventNextDamage() {
- return preventNextDamage;
- }
- public void setPreventNextDamage(final int n) {
- preventNextDamage = n;
- }
- public void addPreventNextDamage(final int n) {
- preventNextDamage += n;
- }
- public void subtractPreventNextDamage(final int n) {
- preventNextDamage -= n;
- }
- public void resetPreventNextDamage() {
- preventNextDamage = 0;
- }
-
- // PreventNextDamageWithEffect
- public Map> getPreventNextDamageWithEffect() {
- return preventionShieldsWithEffects;
- }
public int getPreventNextDamageTotalShields() {
- int shields = preventNextDamage;
- for (final Map value : preventionShieldsWithEffects.values()) {
- shields += Integer.valueOf(value.get("ShieldAmount"));
- }
- return shields;
- }
- /**
- * Adds a damage prevention shield with an effect that happens at time of prevention.
- * @param shieldSource - The source card which generated the shield
- * @param effectMap - A map of the effect occurring with the damage prevention
- */
- public void addPreventNextDamageWithEffect(final Card shieldSource, Map effectMap) {
- if (preventionShieldsWithEffects.containsKey(shieldSource)) {
- int currentShields = Integer.valueOf(preventionShieldsWithEffects.get(shieldSource).get("ShieldAmount"));
- currentShields += Integer.valueOf(effectMap.get("ShieldAmount"));
- effectMap.put("ShieldAmount", Integer.toString(currentShields));
- preventionShieldsWithEffects.put(shieldSource, effectMap);
- } else {
- preventionShieldsWithEffects.put(shieldSource, effectMap);
- }
- }
- public void subtractPreventNextDamageWithEffect(final Card shieldSource, final int n) {
- int currentShields = Integer.valueOf(preventionShieldsWithEffects.get(shieldSource).get("ShieldAmount"));
- if (currentShields > n) {
- preventionShieldsWithEffects.get(shieldSource).put("ShieldAmount", String.valueOf(currentShields - n));
- } else {
- preventionShieldsWithEffects.remove(shieldSource);
- }
- }
- public void resetPreventNextDamageWithEffect() {
- preventionShieldsWithEffects.clear();
+ return getGame().getReplacementHandler().getTotalPreventionShieldAmount(this);
}
public abstract boolean hasKeyword(final String keyword);
diff --git a/forge-game/src/main/java/forge/game/GameEntityCounterTable.java b/forge-game/src/main/java/forge/game/GameEntityCounterTable.java
index 2b881bd9437..222e1a16a58 100644
--- a/forge-game/src/main/java/forge/game/GameEntityCounterTable.java
+++ b/forge-game/src/main/java/forge/game/GameEntityCounterTable.java
@@ -20,6 +20,13 @@ public class GameEntityCounterTable extends ForwardingTable, Ga
private Table, GameEntity, Map> dataMap = HashBasedTable.create();
+ public GameEntityCounterTable() {
+ }
+
+ public GameEntityCounterTable(Table, GameEntity, Map> counterTable) {
+ putAll(counterTable);
+ }
+
/*
* (non-Javadoc)
* @see com.google.common.collect.ForwardingTable#delegate()
diff --git a/forge-game/src/main/java/forge/game/GameFormat.java b/forge-game/src/main/java/forge/game/GameFormat.java
index 026b83ea942..ace852683b6 100644
--- a/forge-game/src/main/java/forge/game/GameFormat.java
+++ b/forge-game/src/main/java/forge/game/GameFormat.java
@@ -101,9 +101,10 @@ public class GameFormat implements Comparable {
this.effectiveDate = effectiveDate;
if(sets != null) {
+ StaticData data = StaticData.instance();
Set parsedSets = new HashSet<>();
for (String set : sets) {
- if (StaticData.instance().getEditions().get(set) == null) {
+ if (data.getCardEdition(set) == null) {
System.out.println("Set " + set + " in format " + fName + " does not match any valid editions!");
continue;
}
diff --git a/forge-game/src/main/java/forge/game/StaticEffect.java b/forge-game/src/main/java/forge/game/StaticEffect.java
index d0ee86b9945..165c351c46f 100644
--- a/forge-game/src/main/java/forge/game/StaticEffect.java
+++ b/forge-game/src/main/java/forge/game/StaticEffect.java
@@ -240,7 +240,7 @@ public class StaticEffect {
// remove keywords
// (Although nothing uses it at this time)
if (hasParam("AddKeyword") || hasParam("RemoveKeyword")
- || hasParam("RemoveAllAbilities")) {
+ || hasParam("RemoveAllAbilities") || hasParam("RemoveLandTypes")) {
affectedCard.removeChangedCardKeywords(getTimestamp());
}
diff --git a/forge-game/src/main/java/forge/game/ability/AbilityKey.java b/forge-game/src/main/java/forge/game/ability/AbilityKey.java
index a3d3089fa63..526173cdd87 100644
--- a/forge-game/src/main/java/forge/game/ability/AbilityKey.java
+++ b/forge-game/src/main/java/forge/game/ability/AbilityKey.java
@@ -57,6 +57,7 @@ public enum AbilityKey {
DefendingPlayer("DefendingPlayer"),
Destination("Destination"),
Devoured("Devoured"),
+ DividedShieldAmount("DividedShieldAmount"),
EchoPaid("EchoPaid"),
EffectOnly("EffectOnly"),
Exploited("Exploited"),
@@ -96,12 +97,14 @@ public enum AbilityKey {
PayingMana("PayingMana"),
Phase("Phase"),
Player("Player"),
+ PreventedAmount("PreventedAmount"),
PreventMap("PreventMap"),
Prevention("Prevention"),
Produced("Produced"),
Regeneration("Regeneration"),
ReplacementEffect("ReplacementEffect"),
ReplacementResult("ReplacementResult"),
+ ReplacementResultMap("ReplacementResultMap"),
Result("Result"),
Scheme("Scheme"),
Source("Source"),
diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java
index bdf020b1a7e..36df10af917 100644
--- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java
+++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java
@@ -1,8 +1,12 @@
package forge.game.ability;
import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -15,6 +19,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
import forge.card.CardStateName;
import forge.card.CardType;
@@ -37,16 +42,19 @@ import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterType;
+import forge.game.card.CardPredicates.Presets;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana;
import forge.game.mana.ManaConversionMatrix;
import forge.game.mana.ManaCostBeingPaid;
+import forge.game.phase.PhaseHandler;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.AbilitySub;
+import forge.game.spellability.OptionalCost;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityRestriction;
@@ -58,6 +66,7 @@ import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.Expressions;
+import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
@@ -318,7 +327,7 @@ public class AbilityUtils {
origin = null;
validFilter = workingCopy[2];
}
- for (final Card cl : CardUtil.getThisTurnEntered(destination, origin, validFilter, hostCard)) {
+ for (final Card cl : CardUtil.getThisTurnEntered(destination, origin, validFilter, hostCard, sa)) {
Card gameState = game.getCardState(cl, null);
// cards that use this should only care about if it is still in that zone
// TODO if all LKI needs to be returned, need to change CardCollection return from this function
@@ -509,33 +518,33 @@ public class AbilityUtils {
if (calcX[0].startsWith("Count")) {
val = AbilityUtils.xCount(card, calcX[1], ability);
} else if (calcX[0].startsWith("Number")) {
- val = CardFactoryUtil.xCount(card, svarval);
+ val = AbilityUtils.xCount(card, svarval, ability);
} else if (calcX[0].startsWith("SVar")) {
final String[] l = calcX[1].split("/");
final String m = CardFactoryUtil.extractOperators(calcX[1]);
- val = CardFactoryUtil.doXMath(AbilityUtils.calculateAmount(card, l[0], ability), m, card);
+ val = doXMath(calculateAmount(card, l[0], ability), m, card, ability);
} else if (calcX[0].startsWith("PlayerCount")) {
final String hType = calcX[0].substring(11);
final FCollection players = new FCollection<>();
if (hType.equals("Players") || hType.equals("")) {
players.addAll(game.getPlayers());
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else if (hType.equals("YourTeam")) {
players.addAll(player.getYourTeam());
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else if (hType.equals("Opponents")) {
players.addAll(player.getOpponents());
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else if (hType.equals("RegisteredOpponents")) {
players.addAll(Iterables.filter(game.getRegisteredPlayers(),PlayerPredicates.isOpponentOf(player)));
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else if (hType.equals("Other")) {
players.addAll(player.getAllOtherPlayers());
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else if (hType.equals("Remembered")) {
for (final Object o : card.getRemembered()) {
@@ -543,12 +552,12 @@ public class AbilityUtils {
players.add((Player) o);
}
}
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else if (hType.equals("NonActive")) {
players.addAll(game.getPlayers());
players.remove(game.getPhaseHandler().getPlayerTurn());
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else if (hType.startsWith("PropertyYou")) {
if (ability instanceof SpellAbility) {
@@ -557,7 +566,7 @@ public class AbilityUtils {
} else {
players.add(player);
}
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else if (hType.startsWith("Property")) {
String defined = hType.split("Property")[1];
@@ -568,25 +577,16 @@ public class AbilityUtils {
players.add(p);
}
}
- val = CardFactoryUtil.playerXCount(players, calcX[1], card);
+ val = playerXCount(players, calcX[1], card, ability);
}
else {
val = 0;
}
}
-
- if (val != null) {
- if (maxto) {
- val = Math.max(val, 0);
- }
- return val * multiplier;
+ else if (calcX[0].equals("OriginalHost")) {
+ val = xCount(ability.getOriginalHost(), calcX[1], ability);
}
-
- if (calcX[0].equals("OriginalHost")) {
- return AbilityUtils.xCount(ability.getOriginalHost(), calcX[1], ability) * multiplier;
- }
-
- if (calcX[0].startsWith("Remembered")) {
+ else if (calcX[0].startsWith("Remembered")) {
// Add whole Remembered list to handlePaid
final CardCollection list = new CardCollection();
Card newCard = card;
@@ -609,10 +609,9 @@ public class AbilityUtils {
}
}
- return CardFactoryUtil.handlePaid(list, calcX[1], card) * multiplier;
+ val = handlePaid(list, calcX[1], card, ability);
}
-
- if (calcX[0].startsWith("Imprinted")) {
+ else if (calcX[0].startsWith("Imprinted")) {
// Add whole Imprinted list to handlePaid
final CardCollection list = new CardCollection();
Card newCard = card;
@@ -629,10 +628,9 @@ public class AbilityUtils {
}
}
- return CardFactoryUtil.handlePaid(list, calcX[1], card) * multiplier;
+ val = handlePaid(list, calcX[1], card, ability);
}
-
- if (calcX[0].matches("Enchanted")) {
+ else if (calcX[0].matches("Enchanted")) {
// Add whole Enchanted list to handlePaid
final CardCollection list = new CardCollection();
if (card.isEnchanting()) {
@@ -641,191 +639,187 @@ public class AbilityUtils {
list.add(game.getCardState((Card) o));
}
}
- return CardFactoryUtil.handlePaid(list, calcX[1], card) * multiplier;
+ val = handlePaid(list, calcX[1], card, ability);
}
// All the following only work for SpellAbilities
- if (!(ability instanceof SpellAbility)) {
- return 0;
- }
-
- final SpellAbility sa = (SpellAbility) ability;
- if (calcX[0].startsWith("Modes")) {
- int chosenModes = 0;
- SpellAbility sub = sa;
- while(sub != null) {
- if (!sub.getSVar("CharmOrder").equals("")) {
- chosenModes++;
+ else if (ability instanceof SpellAbility) {
+ final SpellAbility sa = (SpellAbility) ability;
+ if (calcX[0].startsWith("Modes")) {
+ int chosenModes = 0;
+ SpellAbility sub = sa;
+ while(sub != null) {
+ if (!sub.getSVar("CharmOrder").equals("")) {
+ chosenModes++;
+ }
+ sub = sub.getSubAbility();
}
- sub = sub.getSubAbility();
+ // Count Math
+ final String m = CardFactoryUtil.extractOperators(calcX[1]);
+ val = doXMath(chosenModes, m, card, ability);
}
- // Count Math
- final String m = CardFactoryUtil.extractOperators(calcX[1]);
- return CardFactoryUtil.doXMath(chosenModes, m, card) * multiplier;
- }
-
- // Player attribute counting
- if (calcX[0].startsWith("TargetedPlayer")) {
- final List players = new ArrayList<>();
- final SpellAbility saTargeting = sa.getSATargetingPlayer();
- if (null != saTargeting) {
- Iterables.addAll(players, saTargeting.getTargets().getTargetPlayers());
- }
- return CardFactoryUtil.playerXCount(players, calcX[1], card) * multiplier;
- }
- if (calcX[0].startsWith("ThisTargetedPlayer")) {
- final List players = new ArrayList<>();
- Iterables.addAll(players, sa.getTargets().getTargetPlayers());
- return CardFactoryUtil.playerXCount(players, calcX[1], card) * multiplier;
- }
- if (calcX[0].startsWith("TargetedObjects")) {
- final List objects = new ArrayList<>();
- // Make list of all targeted objects starting with the root SpellAbility
- SpellAbility loopSA = sa.getRootAbility();
- while (loopSA != null) {
- if (loopSA.getTargetRestrictions() != null) {
- Iterables.addAll(objects, loopSA.getTargets());
+ // Player attribute counting
+ else if (calcX[0].startsWith("TargetedPlayer")) {
+ final List players = new ArrayList<>();
+ final SpellAbility saTargeting = sa.getSATargetingPlayer();
+ if (null != saTargeting) {
+ Iterables.addAll(players, saTargeting.getTargets().getTargetPlayers());
}
- loopSA = loopSA.getSubAbility();
+ val = playerXCount(players, calcX[1], card, ability);
}
- return CardFactoryUtil.objectXCount(objects, calcX[1], card) * multiplier;
- }
- if (calcX[0].startsWith("TargetedController")) {
- final List players = new ArrayList<>();
- final CardCollection list = getDefinedCards(card, "Targeted", sa);
- final List sas = AbilityUtils.getDefinedSpellAbilities(card, "Targeted", sa);
+ else if (calcX[0].startsWith("ThisTargetedPlayer")) {
+ final List players = new ArrayList<>();
+ Iterables.addAll(players, sa.getTargets().getTargetPlayers());
+ val = playerXCount(players, calcX[1], card, ability);
+ }
+ else if (calcX[0].startsWith("TargetedObjects")) {
+ final List objects = new ArrayList<>();
+ // Make list of all targeted objects starting with the root SpellAbility
+ SpellAbility loopSA = sa.getRootAbility();
+ while (loopSA != null) {
+ if (loopSA.getTargetRestrictions() != null) {
+ Iterables.addAll(objects, loopSA.getTargets());
+ }
+ loopSA = loopSA.getSubAbility();
+ }
+ val = objectXCount(objects, calcX[1], card, ability);
+ }
+ else if (calcX[0].startsWith("TargetedController")) {
+ final List players = new ArrayList<>();
+ final CardCollection list = getDefinedCards(card, "Targeted", sa);
+ final List sas = AbilityUtils.getDefinedSpellAbilities(card, "Targeted", sa);
- for (final Card c : list) {
- final Player p = c.getController();
- if (!players.contains(p)) {
- players.add(p);
+ for (final Card c : list) {
+ final Player p = c.getController();
+ if (!players.contains(p)) {
+ players.add(p);
+ }
+ }
+ for (final SpellAbility s : sas) {
+ final Player p = s.getHostCard().getController();
+ if (!players.contains(p)) {
+ players.add(p);
+ }
+ }
+ val = playerXCount(players, calcX[1], card, ability);
+ }
+ else if (calcX[0].startsWith("TargetedByTarget")) {
+ final CardCollection tgtList = new CardCollection();
+ final List saList = getDefinedSpellAbilities(card, "Targeted", sa);
+
+ for (final SpellAbility s : saList) {
+ tgtList.addAll(getDefinedCards(s.getHostCard(), "Targeted", s));
+ // Check sub-abilities, so that modal cards like Abzan Charm are correctly handled.
+ // TODO: Should this be done in a more general place, like in getDefinedCards()?
+ AbilitySub abSub = s.getSubAbility();
+ while (abSub != null) {
+ tgtList.addAll(getDefinedCards(abSub.getHostCard(), "Targeted", abSub));
+ abSub = abSub.getSubAbility();
+ }
+ }
+ val = handlePaid(tgtList, calcX[1], card, ability);
+ }
+ else if (calcX[0].startsWith("TriggeredPlayers") || calcX[0].equals("TriggeredCardController")) {
+ String key = calcX[0];
+ if (calcX[0].startsWith("TriggeredPlayers")) {
+ key = "Triggered" + key.substring(16);
+ }
+ val = playerXCount(getDefinedPlayers(card, key, sa), calcX[1], card, ability);
+ }
+ else if (calcX[0].startsWith("TriggeredPlayer") || calcX[0].startsWith("TriggeredTarget")) {
+ final SpellAbility root = sa.getRootAbility();
+ Object o = root.getTriggeringObject(AbilityKey.fromString(calcX[0].substring(9)));
+ val = o instanceof Player ? playerXProperty((Player) o, calcX[1], card, ability) : 0;
+ }
+ else if (calcX[0].equals("TriggeredSpellAbility")) {
+ final SpellAbility root = sa.getRootAbility();
+ SpellAbility sat = (SpellAbility) root.getTriggeringObject(AbilityKey.SpellAbility);
+ val = calculateAmount(sat.getHostCard(), calcX[1], sat);
+ }
+ else if (calcX[0].startsWith("TriggerCount")) {
+ // TriggerCount is similar to a regular Count, but just
+ // pulls Integer Values from Trigger objects
+ 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]));
+
+ val = doXMath(ObjectUtils.firstNonNull(count, 0), m, card, ability);
+ }
+ else if (calcX[0].startsWith("ReplaceCount")) {
+ // ReplaceCount is similar to a regular Count, but just
+ // pulls Integer Values from Replacement objects
+ final SpellAbility root = sa.getRootAbility();
+ final String[] l = calcX[1].split("/");
+ final String m = CardFactoryUtil.extractOperators(calcX[1]);
+ final Integer count = (Integer) root.getReplacingObject(AbilityKey.fromString(l[0]));
+
+ val = doXMath(ObjectUtils.firstNonNull(count, 0), m, card, ability);
+ } else { // these ones only for handling lists
+ Iterable list = null;
+ if (calcX[0].startsWith("Sacrificed")) {
+ list = sa.getRootAbility().getPaidList("Sacrificed");
+ }
+ else if (calcX[0].startsWith("Discarded")) {
+ final SpellAbility root = sa.getRootAbility();
+ list = root.getPaidList("Discarded");
+ if ((null == list) && root.isTrigger()) {
+ list = root.getHostCard().getSpellPermanent().getPaidList("Discarded");
+ }
+ }
+ else if (calcX[0].startsWith("Exiled")) {
+ list = sa.getRootAbility().getPaidList("Exiled");
+ }
+ else if (calcX[0].startsWith("Milled")) {
+ list = sa.getRootAbility().getPaidList("Milled");
+ }
+ else if (calcX[0].startsWith("Tapped")) {
+ list = sa.getRootAbility().getPaidList("Tapped");
+ }
+ else if (calcX[0].startsWith("Revealed")) {
+ list = sa.getRootAbility().getPaidList("Revealed");
+ }
+ else if (calcX[0].startsWith("Targeted")) {
+ list = sa.findTargetedCards();
+ }
+ else if (calcX[0].startsWith("ParentTargeted")) {
+ SpellAbility parent = sa.getParentTargetingCard();
+ if (parent != null) {
+ list = parent.findTargetedCards();
+ }
+ else {
+ list = null;
+ }
+ }
+ else if (calcX[0].startsWith("TriggerRemembered")) {
+ final SpellAbility root = sa.getRootAbility();
+ list = Iterables.filter(root.getTriggerRemembered(), Card.class);
+ }
+ else if (calcX[0].startsWith("TriggerObjects")) {
+ final SpellAbility root = sa.getRootAbility();
+ list = Iterables.filter((Iterable>) root.getTriggeringObject(AbilityKey.fromString(calcX[0].substring(14))), Card.class);
+ }
+ else if (calcX[0].startsWith("Triggered")) {
+ final SpellAbility root = sa.getRootAbility();
+ list = new CardCollection((Card) root.getTriggeringObject(AbilityKey.fromString(calcX[0].substring(9))));
+ }
+ else if (calcX[0].startsWith("Replaced")) {
+ final SpellAbility root = sa.getRootAbility();
+ list = new CardCollection((Card) root.getReplacingObject(AbilityKey.fromString(calcX[0].substring(8))));
+ }
+ if (list != null) {
+ val = handlePaid(list, calcX[1], card, ability);
}
}
- for (final SpellAbility s : sas) {
- final Player p = s.getHostCard().getController();
- if (!players.contains(p)) {
- players.add(p);
- }
- }
- return CardFactoryUtil.playerXCount(players, calcX[1], card) * multiplier;
}
- if (calcX[0].startsWith("TargetedByTarget")) {
- final CardCollection tgtList = new CardCollection();
- final List saList = getDefinedSpellAbilities(card, "Targeted", sa);
- for (final SpellAbility s : saList) {
- tgtList.addAll(getDefinedCards(s.getHostCard(), "Targeted", s));
- // Check sub-abilities, so that modal cards like Abzan Charm are correctly handled.
- // TODO: Should this be done in a more general place, like in getDefinedCards()?
- AbilitySub abSub = s.getSubAbility();
- while (abSub != null) {
- tgtList.addAll(getDefinedCards(abSub.getHostCard(), "Targeted", abSub));
- abSub = abSub.getSubAbility();
- }
+ if (val != null) {
+ if (maxto) {
+ val = Math.max(val, 0);
}
- return CardFactoryUtil.handlePaid(tgtList, calcX[1], card) * multiplier;
+ return val * multiplier;
}
- if (calcX[0].startsWith("TriggeredPlayers") || calcX[0].equals("TriggeredCardController")) {
- String key = calcX[0];
- if (calcX[0].startsWith("TriggeredPlayers")) {
- key = "Triggered" + key.substring(16);
- }
- return CardFactoryUtil.playerXCount(getDefinedPlayers(card, key, sa), calcX[1], card) * multiplier;
- }
- if (calcX[0].startsWith("TriggeredPlayer") || calcX[0].startsWith("TriggeredTarget")) {
- final SpellAbility root = sa.getRootAbility();
- Object o = root.getTriggeringObject(AbilityKey.fromString(calcX[0].substring(9)));
- return o instanceof Player ? CardFactoryUtil.playerXProperty((Player) o, calcX[1], card) * multiplier : 0;
- }
- if (calcX[0].equals("TriggeredSpellAbility")) {
- final SpellAbility root = sa.getRootAbility();
- SpellAbility sat = (SpellAbility) root.getTriggeringObject(AbilityKey.SpellAbility);
- return calculateAmount(sat.getHostCard(), calcX[1], sat);
- }
- // Added on 9/30/12 (ArsenalNut) - Ended up not using but might be useful in future
- /*
- if (calcX[0].startsWith("EnchantedController")) {
- final ArrayList players = new ArrayList();
- players.addAll(AbilityFactory.getDefinedPlayers(card, "EnchantedController", ability));
- return CardFactoryUtil.playerXCount(players, calcX[1], card) * multiplier;
- }
- */
-
- Iterable list;
- if (calcX[0].startsWith("Sacrificed")) {
- list = sa.getRootAbility().getPaidList("Sacrificed");
- }
- else if (calcX[0].startsWith("Discarded")) {
- final SpellAbility root = sa.getRootAbility();
- list = root.getPaidList("Discarded");
- if ((null == list) && root.isTrigger()) {
- list = root.getHostCard().getSpellPermanent().getPaidList("Discarded");
- }
- }
- else if (calcX[0].startsWith("Exiled")) {
- list = sa.getRootAbility().getPaidList("Exiled");
- }
- else if (calcX[0].startsWith("Milled")) {
- list = sa.getRootAbility().getPaidList("Milled");
- }
- else if (calcX[0].startsWith("Tapped")) {
- list = sa.getRootAbility().getPaidList("Tapped");
- }
- else if (calcX[0].startsWith("Revealed")) {
- list = sa.getRootAbility().getPaidList("Revealed");
- }
- else if (calcX[0].startsWith("Targeted")) {
- list = sa.findTargetedCards();
- }
- else if (calcX[0].startsWith("ParentTargeted")) {
- SpellAbility parent = sa.getParentTargetingCard();
- if (parent != null) {
- list = parent.findTargetedCards();
- }
- else {
- list = null;
- }
- }
- else if (calcX[0].startsWith("TriggerRemembered")) {
- final SpellAbility root = sa.getRootAbility();
- list = Iterables.filter(root.getTriggerRemembered(), Card.class);
- }
- else if (calcX[0].startsWith("TriggerObjects")) {
- final SpellAbility root = sa.getRootAbility();
- list = Iterables.filter((Iterable>) root.getTriggeringObject(AbilityKey.fromString(calcX[0].substring(14))), Card.class);
- }
- else if (calcX[0].startsWith("Triggered")) {
- final SpellAbility root = sa.getRootAbility();
- list = new CardCollection((Card) root.getTriggeringObject(AbilityKey.fromString(calcX[0].substring(9))));
- }
- else if (calcX[0].startsWith("TriggerCount")) {
- // TriggerCount is similar to a regular Count, but just
- // pulls Integer Values from Trigger objects
- 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]));
-
- return CardFactoryUtil.doXMath(ObjectUtils.firstNonNull(count, 0), m, card) * multiplier;
- }
- else if (calcX[0].startsWith("Replaced")) {
- final SpellAbility root = sa.getRootAbility();
- list = new CardCollection((Card) root.getReplacingObject(AbilityKey.fromString(calcX[0].substring(8))));
- }
- else if (calcX[0].startsWith("ReplaceCount")) {
- // ReplaceCount is similar to a regular Count, but just
- // pulls Integer Values from Replacement objects
- final SpellAbility root = sa.getRootAbility();
- final String[] l = calcX[1].split("/");
- final String m = CardFactoryUtil.extractOperators(calcX[1]);
- final Integer count = (Integer) root.getReplacingObject(AbilityKey.fromString(l[0]));
-
- return CardFactoryUtil.doXMath(ObjectUtils.firstNonNull(count, 0), m, card) * multiplier;
- }
- else {
- return 0;
- }
- return CardFactoryUtil.handlePaid(list, calcX[1], card) * multiplier;
+ return 0;
}
/**
@@ -1239,7 +1233,6 @@ public class AbilityUtils {
}
else if (defined.startsWith("PlayerNamed_")) {
for (Player p : game.getPlayersInTurnOrder()) {
- //System.out.println("Named player " + defined.substring(12));
if (p.getName().equals(defined.substring(12))) {
players.add(p);
}
@@ -1558,7 +1551,7 @@ public class AbilityUtils {
boolean alreadyPaid = false;
for (Player payer : allPayers) {
if (unlessCost.equals("LifeTotalHalfUp")) {
- String halfup = Integer.toString((int) Math.ceil(payer.getLife() / 2.0));
+ String halfup = Integer.toString(Math.max(0,(int) Math.ceil(payer.getLife() / 2.0)));
cost = new Cost("PayLife<" + halfup + ">", true);
}
alreadyPaid |= payer.getController().payCostToPreventEffect(cost, sa, alreadyPaid, allPayers);
@@ -1636,6 +1629,8 @@ public class AbilityUtils {
}
}
}
+ // make sure that when this is from a trigger LKI is updated
+ host.getGame().updateLastStateForCard(host);
}
/**
@@ -1657,6 +1652,26 @@ public class AbilityUtils {
final String expr = CardFactoryUtil.extractOperators(s2);
final Player player = ctb == null ? null : ctb instanceof SpellAbility ? ((SpellAbility)ctb).getActivatingPlayer() : ctb.getHostCard().getController();
+ // accept straight numbers
+ if (l[0].startsWith("Number$")) {
+ final String number = l[0].substring(7);
+ if (number.equals("ChosenNumber")) { // TODO remove in favor of Count ChosenNumber
+ int x = c.getChosenNumber() == null ? 0 : c.getChosenNumber();
+ return doXMath(x, expr, c, ctb);
+ }
+ return doXMath(Integer.parseInt(number), expr, c, ctb);
+ }
+
+ if (l[0].startsWith("Count$")) {
+ l[0] = l[0].substring(6);
+ }
+
+ if (l[0].startsWith("SVar$")) {
+ String n = l[0].substring(5);
+ String v = ctb == null ? c.getSVar(n) : ctb.getSVar(n);
+ return doXMath(xCount(c, v, ctb), expr, c, ctb);
+ }
+
final String[] sq;
sq = l[0].split("\\.");
@@ -1668,12 +1683,8 @@ public class AbilityUtils {
final String[] compString = sq[0].split(" ");
final int lhs = calculateAmount(c, compString[1], ctb);
final int rhs = calculateAmount(c, compString[2].substring(2), ctb);
- if (Expressions.compare(lhs, compString[2], rhs)) {
- return CardFactoryUtil.doXMath(calculateAmount(c, sq[1], ctb), expr, c);
- }
- else {
- return CardFactoryUtil.doXMath(calculateAmount(c, sq[2], ctb), expr, c);
- }
+ boolean v = Expressions.compare(lhs, compString[2], rhs);
+ return doXMath(calculateAmount(c, sq[v ? 1 : 2], ctb), expr, c, ctb);
}
if (ctb instanceof SpellAbility) {
final SpellAbility sa = (SpellAbility) ctb;
@@ -1685,19 +1696,19 @@ public class AbilityUtils {
// 107.3i If an object gains an ability, the value of X within that ability is the value defined by that ability,
// or 0 if that ability doesn’t define a value of X. This is an exception to rule 107.3h. This may occur with ability-adding effects, text-changing effects, or copy effects.
if (root.getXManaCostPaid() != null) {
- return CardFactoryUtil.doXMath(root.getXManaCostPaid(), expr, c);
+ return doXMath(root.getXManaCostPaid(), expr, c, ctb);
}
if (root.isTrigger()) {
Trigger t = root.getTrigger();
if (t == null) {
- return CardFactoryUtil.doXMath(0, expr, c);
+ return doXMath(0, expr, c, ctb);
}
// ImmediateTrigger should check for the Ability which created the trigger
if (t.getSpawningAbility() != null) {
root = t.getSpawningAbility().getRootAbility();
- return CardFactoryUtil.doXMath(root.getXManaCostPaid(), expr, c);
+ return doXMath(root.getXManaCostPaid(), expr, c, ctb);
}
// 107.3k If an object’s enters-the-battlefield triggered ability or replacement effect refers to X,
@@ -1705,68 +1716,54 @@ public class AbilityUtils {
// the value of X for that ability is the same as the value of X for that spell, although the value of X for that permanent is 0.
if (TriggerType.ChangesZone.equals(t.getMode())
&& ZoneType.Battlefield.name().equals(t.getParam("Destination"))) {
- return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c);
+ return doXMath(c.getXManaCostPaid(), expr, c, ctb);
} else if (TriggerType.SpellCast.equals(t.getMode())) {
// Cast Trigger like Hydroid Krasis
SpellAbility castSA = (SpellAbility) root.getTriggeringObject(AbilityKey.SpellAbility);
if (castSA == null || castSA.getXManaCostPaid() == null) {
- return CardFactoryUtil.doXMath(0, expr, c);
+ return doXMath(0, expr, c, ctb);
}
- return CardFactoryUtil.doXMath(castSA.getXManaCostPaid(), expr, c);
+ return doXMath(castSA.getXManaCostPaid(), expr, c, ctb);
} else if (TriggerType.Cycled.equals(t.getMode())) {
SpellAbility cycleSA = (SpellAbility) sa.getTriggeringObject(AbilityKey.Cause);
if (cycleSA == null || cycleSA.getXManaCostPaid() == null) {
- return CardFactoryUtil.doXMath(0, expr, c);
+ return doXMath(0, expr, c, ctb);
}
- return CardFactoryUtil.doXMath(cycleSA.getXManaCostPaid(), expr, c);
+ return doXMath(cycleSA.getXManaCostPaid(), expr, c, ctb);
} else if (TriggerType.TurnFaceUp.equals(t.getMode())) {
SpellAbility turnupSA = (SpellAbility) sa.getTriggeringObject(AbilityKey.Cause);
if (turnupSA == null || turnupSA.getXManaCostPaid() == null) {
- return CardFactoryUtil.doXMath(0, expr, c);
+ return doXMath(0, expr, c, ctb);
}
- return CardFactoryUtil.doXMath(turnupSA.getXManaCostPaid(), expr, c);
+ return doXMath(turnupSA.getXManaCostPaid(), expr, c, ctb);
}
}
// If the chosen creature has X in its mana cost, that X is considered to be 0.
// The value of X in Altered Ego’s last ability will be whatever value was chosen for X while casting Altered Ego.
if (sa.isCopiedTrait() || !sa.getHostCard().equals(c)) {
- return CardFactoryUtil.doXMath(0, expr, c);
+ return doXMath(0, expr, c, ctb);
}
if (root.isReplacementAbility()) {
if (sa.hasParam("ETB")) {
- return CardFactoryUtil.doXMath(c.getXManaCostPaid(), expr, c);
+ return doXMath(c.getXManaCostPaid(), expr, c, ctb);
}
}
- return CardFactoryUtil.doXMath(0, expr, c);
+ return doXMath(0, expr, c, ctb);
}
// Count$Kicked..
if (sq[0].startsWith("Kicked")) {
boolean kicked = sa.isKicked() || c.getKickerMagnitude() > 0;
- return CardFactoryUtil.doXMath(Integer.parseInt(kicked ? sq[1] : sq[2]), expr, c);
+ return doXMath(Integer.parseInt(kicked ? sq[1] : sq[2]), expr, c, ctb);
}
- // Count$UrzaLands..
- if (sq[0].startsWith("UrzaLands")) {
- return CardFactoryUtil.doXMath(Integer.parseInt(sa.getActivatingPlayer().hasUrzaLands() ? sq[1] : sq[2]), expr, c);
- }
-
- //Count$SearchedLibrary.
- if (sq[0].contains("SearchedLibrary")) {
- int sum = 0;
- for (Player p : AbilityUtils.getDefinedPlayers(c, sq[1], sa)) {
- sum += p.getLibrarySearched();
- }
-
- return sum;
- }
//Count$HasNumChosenColors.
if (sq[0].contains("HasNumChosenColors")) {
int sum = 0;
- for (Card card : AbilityUtils.getDefinedCards(sa.getHostCard(), sq[1], sa)) {
+ for (Card card : AbilityUtils.getDefinedCards(c, sq[1], sa)) {
sum += CardUtil.getColors(card).getSharedColors(ColorSet.fromNames(c.getChosenColors())).countColors();
}
return sum;
@@ -1792,69 +1789,44 @@ public class AbilityUtils {
}
return count;
}
- // Count$AttachedTo
- if (sq[0].startsWith("AttachedTo")) {
- final String[] k = l[0].split(" ");
- int sum = 0;
- for (Card card : AbilityUtils.getDefinedCards(sa.getHostCard(), k[1], sa)) {
- // Hateful Eidolon: the script uses LKI so that the attached cards have to be defined
- // This card needs the spellability ("Auras You control", you refers to the activating player)
- // CardFactoryUtils.xCount doesn't have the sa parameter, SVar:X:TriggeredCard$Valid cannot handle this
- CardCollection list = CardLists.getValidCards(card.getAttachedCards(), k[2].split(","), sa.getActivatingPlayer(), c, sa);
- sum += list.size();
- }
- return sum;
- }
// Count$Adamant...
if (sq[0].startsWith("Adamant")) {
final String payingMana = StringUtils.join(sa.getRootAbility().getPayingMana());
final int num = sq[0].length() > 7 ? Integer.parseInt(sq[0].split("_")[1]) : 3;
final boolean adamant = StringUtils.countMatches(payingMana, MagicColor.toShortString(sq[1])) >= num;
- return CardFactoryUtil.doXMath(Integer.parseInt(sq[adamant ? 2 : 3]), expr, c);
+ return doXMath(calculateAmount(c,sq[adamant ? 2 : 3], ctb), expr, c, ctb);
}
- if (l[0].startsWith("LastStateBattlefield")) {
+ if (sq[0].startsWith("LastStateBattlefield")) {
final String[] k = l[0].split(" ");
CardCollectionView list = null;
if (sa.getLastStateBattlefield() != null) {
list = sa.getLastStateBattlefield();
} else { // LastState is Empty
- return CardFactoryUtil.doXMath(0, expr, c);
+ return doXMath(0, expr, c, ctb);
}
list = CardLists.getValidCards(list, k[1].split(","), sa.getActivatingPlayer(), c, sa);
if (k[0].contains("TotalToughness")) {
- return CardFactoryUtil.doXMath(Aggregates.sum(list, CardPredicates.Accessors.fnGetNetToughness), expr, c);
+ return doXMath(Aggregates.sum(list, CardPredicates.Accessors.fnGetNetToughness), expr, c, ctb);
} else {
- return CardFactoryUtil.doXMath(list.size(), expr, c);
+ return doXMath(list.size(), expr, c, ctb);
}
}
- if (l[0].startsWith("LastStateGraveyard")) {
+ if (sq[0].startsWith("LastStateGraveyard")) {
final String[] k = l[0].split(" ");
CardCollectionView list = null;
if (sa.getLastStateGraveyard() != null) {
list = sa.getLastStateGraveyard();
} else { // LastState is Empty
- return CardFactoryUtil.doXMath(0, expr, c);
+ return doXMath(0, expr, c, ctb);
}
list = CardLists.getValidCards(list, k[1].split(","), sa.getActivatingPlayer(), c, sa);
- return CardFactoryUtil.doXMath(list.size(), expr, c);
- }
-
- // Count$TargetedLifeTotal (targeted player's life total)
- // Not optimal but since xCount doesn't take SAs, we need to replicate while we have it
- // Probably would be best if xCount took an optional SA to use in these circumstances
- if (sq[0].contains("TargetedLifeTotal")) {
- final SpellAbility saTargeting = sa.getSATargetingPlayer();
- if (saTargeting != null) {
- for (final Player tgtP : saTargeting.getTargets().getTargetPlayers()) {
- return CardFactoryUtil.doXMath(tgtP.getLife(), expr, c);
- }
- }
+ return doXMath(list.size(), expr, c, ctb);
}
if (sq[0].startsWith("CastTotalManaSpent")) {
- return CardFactoryUtil.doXMath(c.getCastSA() != null ? c.getCastSA().getTotalManaSpent() : 0, expr, c);
+ return doXMath(c.getCastSA() != null ? c.getCastSA().getTotalManaSpent() : 0, expr, c, ctb);
}
if (sq[0].equals("CastTotalSnowManaSpent")) {
@@ -1866,35 +1838,954 @@ public class AbilityUtils {
}
}
}
- return CardFactoryUtil.doXMath(v, expr, c);
+ return doXMath(v, expr, c, ctb);
}
if (sq[0].equals("ResolvedThisTurn")) {
- return CardFactoryUtil.doXMath(sa.getResolvedThisTurn(), expr, c);
+ return doXMath(sa.getResolvedThisTurn(), expr, c, ctb);
+ }
+ } else {
+ // fallback if ctb isn't a spellability
+ if (sq[0].startsWith("LastStateBattlefield")) {
+ final String[] k = l[0].split(" ");
+ CardCollection list = new CardCollection(game.getLastStateBattlefield());
+ list = CardLists.getValidCards(list, k[1].split(","), player, c, ctb);
+ return doXMath(list.size(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("LastStateGraveyard")) {
+ final String[] k = l[0].split(" ");
+ CardCollection list = new CardCollection(game.getLastStateGraveyard());
+ list = CardLists.getValidCards(list, k[1].split(","), player, c, ctb);
+ return doXMath(list.size(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("xPaid")) {
+ return doXMath(c.getXManaCostPaid(), expr, c, ctb);
+ }
+ } // end SpellAbility
+
+ // Count$TargetedLifeTotal (targeted player's life total)
+ // Not optimal but since xCount doesn't take SAs, we need to replicate while we have it
+ // Probably would be best if xCount took an optional SA to use in these circumstances
+ if (sq[0].contains("TargetedLifeTotal")) {
+ for (Player tgtP : AbilityUtils.getDefinedPlayers(c, "TargetedPlayer", ctb)) {
+ return doXMath(tgtP.getLife(), expr, c, ctb);
}
}
- if (l[0].startsWith("CountersAddedThisTurn")) {
- final String[] parts = l[0].split(" ");
- CounterType cType = CounterType.getType(parts[1]);
-
- return CardFactoryUtil.doXMath(game.getCounterAddedThisTurn(cType, parts[2], parts[3], c, player, ctb), expr, c);
+ // Count$DevotionDual..
+ // Count$Devotion.
+ if (sq[0].contains("Devotion")) {
+ int colorOcurrencices = 0;
+ String colorName = sq[1];
+ if (colorName.contains("Chosen")) {
+ colorName = MagicColor.toShortString(c.getChosenColor());
+ }
+ byte colorCode = ManaAtom.fromName(colorName);
+ if (sq[0].equals("DevotionDual")) {
+ colorCode |= ManaAtom.fromName(sq[2]);
+ }
+ for (Card c0 : player.getCardsIn(ZoneType.Battlefield)) {
+ for (ManaCostShard sh : c0.getManaCost()) {
+ if (sh.isColor(colorCode)) {
+ colorOcurrencices++;
+ }
+ }
+ colorOcurrencices += c0.getAmountOfKeyword("Your devotion to each color and each combination of colors is increased by one.");
+ }
+ return doXMath(colorOcurrencices, expr, c, ctb);
}
- // count valid cards in any specified zone/s
- if (l[0].startsWith("Valid")) {
- String[] lparts = l[0].split(" ", 2);
- final String[] rest = lparts[1].split(",");
+ } // end ctb != null
- final CardCollectionView cardsInZones = lparts[0].length() > 5
- ? game.getCardsIn(ZoneType.listValueOf(lparts[0].substring(5)))
- : game.getCardsIn(ZoneType.Battlefield);
-
- CardCollection cards = CardLists.getValidCards(cardsInZones, rest, player, c, ctb);
- return CardFactoryUtil.doXMath(cards.size(), expr, c);
+ if (sq[0].contains("OppsAtLifeTotal")) {
+ final int lifeTotal = AbilityUtils.calculateAmount(c, sq[1], ctb);
+ int number = 0;
+ for (final Player opp : player.getOpponents()) {
+ if (opp.getLife() == lifeTotal) {
+ number++;
+ }
}
+ return doXMath(number, expr, c, ctb);
}
- return CardFactoryUtil.xCount(c, s2);
+
+ //Count$SearchedLibrary.
+ if (sq[0].contains("SearchedLibrary")) {
+ int sum = 0;
+ for (Player p : AbilityUtils.getDefinedPlayers(c, sq[1], ctb)) {
+ sum += p.getLibrarySearched();
+ }
+ return doXMath(sum, expr, c, ctb);
+ }
+
+ ////////////////////
+ // card info
+
+ // Count$CardMulticolor..
+ if (sq[0].contains("CardMulticolor")) {
+ final boolean isMulti = CardUtil.getColors(c).isMulticolor();
+ return doXMath(Integer.parseInt(sq[isMulti ? 1 : 2]), expr, c, ctb);
+ }
+ // Count$Madness..
+ if (sq[0].startsWith("Madness")) {
+ return doXMath(calculateAmount(c, sq[c.isMadness() ? 1 : 2], ctb), expr, c, ctb);
+ }
+
+ // Count$Foretold..
+ if (sq[0].startsWith("Foretold")) {
+ return doXMath(calculateAmount(c, sq[c.isForetold() ? 1 : 2], ctb), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("Kicked")) { // fallback for not spellAbility
+ return doXMath(calculateAmount(c, sq[c.getKickerMagnitude() > 0 ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].startsWith("Escaped")) {
+ return doXMath(calculateAmount(c, sq[c.getCastSA() != null && c.getCastSA().isEscape() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].startsWith("AltCost")) {
+ return doXMath(calculateAmount(c, sq[c.isOptionalCostPaid(OptionalCost.AltCost) ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("ColorsColorIdentity")) {
+ return doXMath(c.getController().getCommanderColorID().countColors(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("TotalDamageDoneByThisTurn")) {
+ return doXMath(c.getTotalDamageDoneBy(), expr, c, ctb);
+ }
+ if (sq[0].equals("TotalDamageReceivedThisTurn")) {
+ return doXMath(c.getTotalDamageReceivedThisTurn(), expr, c, ctb);
+ }
+
+ if (sq[0].contains("CardPower")) {
+ return doXMath(c.getNetPower(), expr, c, ctb);
+ }
+ if (sq[0].contains("CardToughness")) {
+ return doXMath(c.getNetToughness(), expr, c, ctb);
+ }
+ if (sq[0].contains("CardSumPT")) {
+ return doXMath((c.getNetPower() + c.getNetToughness()), expr, c, ctb);
+ }
+ if (sq[0].contains("CardNumTypes")) {
+ Card ce;
+ if (sq[0].contains("Remembered")) {
+ ce = (Card) c.getFirstRemembered();
+ } else {
+ ce = c;
+ }
+ return doXMath(getNumberOfTypes(ce), expr, c, ctb);
+ }
+
+ if (sq[0].contains("CardNumColors")) {
+ return doXMath(CardUtil.getColors(c).countColors(), expr, c, ctb);
+ }
+ if (sq[0].contains("CardNumAttacksThisTurn")) {
+ return doXMath(c.getDamageHistory().getCreatureAttacksThisTurn(), expr, c, ctb);
+ }
+
+ if (sq[0].contains("CardCounters")) {
+ // CardCounters.ALL to be used for Kinsbaile Borderguard and anything that cares about all counters
+ int count = 0;
+ if (sq[1].equals("ALL")) {
+ for (Integer i : c.getCounters().values()) {
+ if (i != null && i > 0) {
+ count += i;
+ }
+ }
+ }
+ else {
+ count = c.getCounters(CounterType.getType(sq[1]));
+ }
+ return doXMath(count, expr, c, ctb);
+ }
+
+ if (sq[0].contains("BushidoPoint")) {
+ return doXMath(c.getKeywordMagnitude(Keyword.BUSHIDO), expr, c, ctb);
+ }
+ if (sq[0].contains("TimesKicked")) {
+ return doXMath(c.getKickerMagnitude(), expr, c, ctb);
+ }
+ if (sq[0].contains("TimesPseudokicked")) {
+ return doXMath(c.getPseudoKickerMagnitude(), expr, c, ctb);
+ }
+ if (sq[0].contains("TimesMutated")) {
+ return doXMath(c.getTimesMutated(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("DamageDoneByPlayerThisTurn")) {
+ int sum = 0;
+ for (Player p : AbilityUtils.getDefinedPlayers(c, sq[1], ctb)) {
+ sum += c.getReceivedDamageByPlayerThisTurn(p);
+ }
+ return doXMath(sum, expr, c, ctb);
+ }
+ if (sq[0].equals("RegeneratedThisTurn")) {
+ return doXMath(c.getRegeneratedThisTurn(), expr, c, ctb);
+ }
+
+ // Count$Converge
+ if (sq[0].contains("Converge")) {
+ SpellAbility castSA = c.getCastSA();
+ return doXMath(castSA == null ? 0 : castSA.getPayingColors().countColors(), expr, c, ctb);
+ }
+
+ // Count$wasCastFrom..
+ if (sq[0].startsWith("wasCastFrom")) {
+ boolean zonesMatch = c.getCastFrom() == ZoneType.smartValueOf(sq[0].substring(11));
+ return doXMath(calculateAmount(c, sq[zonesMatch ? 1 : 2], ctb), expr, c, ctb);
+ }
+
+ // Count$Presence_..
+ if (sq[0].startsWith("Presence")) {
+ final String type = sq[0].split("_")[1];
+ boolean found = false;
+ if (c.getCastFrom() != null && c.getCastSA() != null) {
+ int revealed = AbilityUtils.calculateAmount(c, "Revealed$Valid " + type, c.getCastSA());
+ int ctrl = AbilityUtils.calculateAmount(c, "Count$Valid " + type + ".inZoneBattlefield+YouCtrl", c.getCastSA());
+ if (revealed + ctrl >= 1) {
+ found = true;
+ }
+ }
+ return doXMath(calculateAmount(c, sq[found ? 1 : 2], ctb), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("Devoured")) {
+ final String validDevoured = sq[0].split(" ")[1];
+ CardCollection cl = CardLists.getValidCards(c.getDevouredCards(), validDevoured.split(","), player, c, ctb);
+ return doXMath(cl.size(), expr, c, ctb);
+ }
+
+ if (sq[0].contains("ChosenNumber")) {
+ Integer i = c.getChosenNumber();
+ return doXMath(i == null ? 0 : i, expr, c, ctb);
+ }
+
+ // Count$IfCastInOwnMainPhase.. // 7/10
+ if (sq[0].contains("IfCastInOwnMainPhase")) {
+ final PhaseHandler cPhase = player.getGame().getPhaseHandler();
+ final boolean isMyMain = cPhase.getPhase().isMain() && cPhase.isPlayerTurn(player) && c.getCastFrom() != null;
+ return doXMath(Integer.parseInt(sq[isMyMain ? 1 : 2]), expr, c, ctb);
+ }
+
+ // Count$AttachedTo
+ if (sq[0].startsWith("AttachedTo")) {
+ final String[] k = l[0].split(" ");
+ int sum = 0;
+ for (Card card : AbilityUtils.getDefinedCards(c, k[1], ctb)) {
+ // Hateful Eidolon: the script uses LKI so that the attached cards have to be defined
+ // This card needs the spellability ("Auras You control", you refers to the activating player)
+ // CardFactoryUtils.xCount doesn't have the sa parameter, SVar:X:TriggeredCard$Valid cannot handle this
+ sum += CardLists.getValidCardCount(card.getAttachedCards(), k[2], player, c, ctb);
+ }
+ return doXMath(sum, expr, c, ctb);
+ }
+
+ // Count$CardManaCost
+ if (sq[0].contains("CardManaCost")) {
+ Card ce;
+ if (sq[0].contains("Equipped") && c.isEquipping()) {
+ ce = c.getEquipping();
+ }
+ else if (sq[0].contains("Remembered")) {
+ ce = (Card) c.getFirstRemembered();
+ }
+ else {
+ ce = c;
+ }
+
+ return doXMath(ce == null ? 0 : ce.getCMC(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("RememberedSize")) {
+ return doXMath(c.getRememberedCount(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("RememberedNumber")) {
+ int num = 0;
+ for (final Object o : c.getRemembered()) {
+ if (o instanceof Integer) {
+ num += (Integer) o;
+ }
+ }
+ return doXMath(num, expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("RememberedWithSharedCardType")) {
+ int maxNum = 1;
+ for (final Object o : c.getRemembered()) {
+ if (o instanceof Card) {
+ int num = 1;
+ Card firstCard = (Card) o;
+ for (final Object p : c.getRemembered()) {
+ if (p instanceof Card) {
+ Card secondCard = (Card) p;
+ if (!firstCard.equals(secondCard) && firstCard.sharesCardTypeWith(secondCard)) {
+ num++;
+ }
+ }
+ }
+ if (num > maxNum) {
+ maxNum = num;
+ }
+ }
+ }
+ return doXMath(maxNum, expr, c, ctb);
+ }
+
+ // Count$EnchantedControllerCreatures
+ if (sq[0].equals("EnchantedControllerCreatures")) { // maybe refactor into a Valid with ControlledBy
+ int v = 0;
+ if (c.getEnchantingCard() != null) {
+ v = CardLists.count(c.getEnchantingCard().getController().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
+ }
+ return doXMath(v, expr, c, ctb);
+ }
+
+ ////////////////////////
+ // player info
+ if (sq[0].equals("Hellbent")) {
+ return doXMath(calculateAmount(c, sq[player.hasHellbent() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("Metalcraft")) {
+ return doXMath(calculateAmount(c, sq[player.hasMetalcraft() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("Delirium")) {
+ return doXMath(calculateAmount(c, sq[player.hasDelirium() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("FatefulHour")) {
+ return doXMath(calculateAmount(c, sq[player.getLife() <= 5 ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("Revolt")) {
+ return doXMath(calculateAmount(c, sq[player.hasRevolt() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("Landfall")) {
+ return doXMath(calculateAmount(c, sq[player.hasLandfall() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("Monarch")) {
+ return doXMath(calculateAmount(c, sq[player.isMonarch() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("Blessing")) {
+ return doXMath(calculateAmount(c, sq[player.hasBlessing() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("Threshold")) {
+ return doXMath(calculateAmount(c, sq[player.hasThreshold() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("ExtraTurn")) {
+ return doXMath(calculateAmount(c, sq[game.getPhaseHandler().getPlayerTurn().isExtraTurn() ? 1 : 2], ctb), expr, c, ctb);
+ }
+ if (sq[0].equals("Averna")) {
+ String str = "As you cascade, you may put a land card from among the exiled cards onto the " +
+ "battlefield tapped.";
+ return doXMath(player.getKeywords().getAmount(str), expr, c, ctb);
+ }
+ if (sq[0].equals("YourStartingLife")) {
+ return doXMath(player.getStartingLife(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("YourLifeTotal")) {
+ return doXMath(player.getLife(), expr, c, ctb);
+ }
+ if (sq[0].equals("OppGreatestLifeTotal")) {
+ return doXMath(player.getOpponentsGreatestLifeTotal(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("YouCycledThisTurn")) {
+ return doXMath(player.getCycledThisTurn(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("YouDrewThisTurn")) {
+ return doXMath(player.getNumDrawnThisTurn(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("YouSurveilThisTurn")) {
+ return doXMath(player.getSurveilThisTurn(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("YouCastThisGame")) {
+ return doXMath(player.getSpellsCastThisGame(), expr, c, ctb);
+ }
+
+ if (sq[0].contains("CardControllerTypes")) {
+ return doXMath(getCardTypesFromList(player.getCardsIn(ZoneType.listValueOf(sq[1]))), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("CommanderCastFromCommandZone")) {
+ // only used by Opal Palace, and it does add the trigger to the card
+ return doXMath(player.getCommanderCast(c), expr, c, ctb);
+ }
+
+ if (l[0].startsWith("TotalCommanderCastFromCommandZone")) {
+ return doXMath(player.getTotalCommanderCast(), expr, c, ctb);
+ }
+
+ if (sq[0].contains("LifeYouLostThisTurn")) {
+ return doXMath(player.getLifeLostThisTurn(), expr, c, ctb);
+ }
+ if (sq[0].contains("LifeYouGainedThisTurn")) {
+ return doXMath(player.getLifeGainedThisTurn(), expr, c, ctb);
+ }
+ if (sq[0].contains("LifeYourTeamGainedThisTurn")) {
+ return doXMath(player.getLifeGainedByTeamThisTurn(), expr, c, ctb);
+ }
+ if (sq[0].contains("LifeYouGainedTimesThisTurn")) {
+ return doXMath(player.getLifeGainedTimesThisTurn(), expr, c, ctb);
+ }
+ if (sq[0].contains("LifeOppsLostThisTurn")) {
+ return doXMath(player.getOpponentLostLifeThisTurn(), expr, c, ctb);
+ }
+ if (sq[0].equals("BloodthirstAmount")) {
+ return doXMath(player.getBloodthirstAmount(), expr, c, ctb);
+ }
+ if (sq[0].equals("YourLandsPlayed")) {
+ return doXMath(player.getLandsPlayedThisTurn(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("YourCounters")) {
+ // "YourCountersExperience" or "YourCountersPoison"
+ String counterType = sq[0].substring(12);
+ return doXMath(player.getCounters(CounterType.getType(counterType)), expr, c, ctb);
+ }
+
+ if (sq[0].contains("YourPoisonCounters")) {
+ return doXMath(player.getPoisonCounters(), expr, c, ctb);
+ }
+ if (sq[0].contains("TotalOppPoisonCounters")) {
+ return doXMath(player.getOpponentsTotalPoisonCounters(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("YourDamageThisTurn")) {
+ return doXMath(player.getAssignedDamage(), expr, c, ctb);
+ }
+ if (sq[0].equals("TotalOppDamageThisTurn")) {
+ return doXMath(player.getOpponentsAssignedDamage(), expr, c, ctb);
+ }
+ if (sq[0].equals("MaxOppDamageThisTurn")) {
+ return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("YourDamageSourcesThisTurn")) {
+ Iterable allSrc = player.getAssignedDamageSources();
+ String restriction = sq[0].split(" ")[1];
+ return doXMath(CardLists.getValidCardCount(allSrc, restriction, player, c, ctb), expr, c, ctb);
+ }
+
+ if (sq[0].equals("YourTurns")) {
+ return doXMath(player.getTurn(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("OppTypesInGrave")) {
+ final PlayerCollection opponents = player.getOpponents();
+ CardCollection oppCards = new CardCollection();
+ oppCards.addAll(opponents.getCardsIn(ZoneType.Graveyard));
+ return doXMath(getCardTypesFromList(oppCards), expr, c, ctb);
+ }
+
+ // Count$TopOfLibraryCMC
+ if (sq[0].equals("TopOfLibraryCMC")) {
+ int cmc = player.getCardsIn(ZoneType.Library).isEmpty() ? 0 :
+ player.getCardsIn(ZoneType.Library).getFirst().getCMC();
+ return doXMath(cmc, expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("ColorsCtrl")) {
+ final String restriction = l[0].substring(11);
+ final String[] rest = restriction.split(",");
+ final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
+ byte n = 0;
+ for (final Card card : list) {
+ n |= card.determineColor().getColor();
+ }
+ return doXMath(ColorSet.fromMask(n).countColors(), expr, c, ctb);
+ }
+
+ // Count$AttackersDeclared
+ if (sq[0].startsWith("AttackersDeclared")) {
+ return doXMath(player.getAttackersDeclaredThisTurn(), expr, c, ctb);
+ }
+
+ // Count$CardAttackedThisTurn
+ if (sq[0].startsWith("CreaturesAttackedThisTurn")) {
+ final String[] workingCopy = l[0].split(" ", 2);
+ final String validFilter = workingCopy[1];
+ return doXMath(CardLists.getValidCardCount(player.getCreaturesAttackedThisTurn(), validFilter, player, c, ctb), expr, c, ctb);
+ }
+
+ // Manapool
+ if (sq[0].startsWith("ManaPool")) {
+ final String color = l[0].split(":")[1];
+ int v = 0;
+ if (color.equals("All")) {
+ v = player.getManaPool().totalMana();
+ } else {
+ v = player.getManaPool().getAmountOfColor(ManaAtom.fromName(color));
+ }
+ return doXMath(v, expr, c, ctb);
+ }
+
+ // Count$Domain
+ if (sq[0].startsWith("Domain")) {
+ int n = 0;
+ Player neededPlayer = sq[0].equals("DomainActivePlayer") ? game.getPhaseHandler().getPlayerTurn() : player;
+ CardCollection someCards = CardLists.filter(neededPlayer.getCardsIn(ZoneType.Battlefield), Presets.LANDS);
+ for (String basic : MagicColor.Constant.BASIC_LANDS) {
+ if (!CardLists.getType(someCards, basic).isEmpty()) {
+ n++;
+ }
+ }
+ return doXMath(n, expr, c, ctb);
+ }
+
+ //SacrificedThisTurn
+ if (sq[0].startsWith("SacrificedThisTurn")) {
+ CardCollectionView list = player.getSacrificedThisTurn();
+ if (l[0].contains(" ")) {
+ String[] lparts = l[0].split(" ", 2);
+ String restrictions = TextUtil.fastReplace(l[0], TextUtil.addSuffix(lparts[0]," "), "");
+ final String[] rest = restrictions.split(",");
+ list = CardLists.getValidCards(list, rest, player, c, ctb);
+ }
+ return doXMath(list.size(), expr, c, ctb);
+ }
+
+ if (sq[0].contains("Party")) {
+ CardCollection adventurers = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield),
+ "Creature.Cleric,Creature.Rogue,Creature.Warrior,Creature.Wizard", player, c, ctb);
+
+ Set partyTypes = Sets.newHashSet("Cleric", "Rogue", "Warrior", "Wizard");
+ int partySize = 0;
+
+ HashMap chosenParty = new HashMap<>();
+ List wildcard = Lists.newArrayList();
+ HashMap> multityped = new HashMap<>();
+
+ // Figure out how to count each class separately.
+ for (Card card : adventurers) {
+ Set creatureTypes = card.getType().getCreatureTypes();
+ boolean anyType = creatureTypes.contains(CardType.AllCreatureTypes);
+ creatureTypes.retainAll(partyTypes);
+
+ if (anyType || creatureTypes.size() == 4) {
+ wildcard.add(card);
+
+ if (wildcard.size() >= 4) {
+ break;
+ }
+ continue;
+ } else if (creatureTypes.size() == 1) {
+ String type = (String)(creatureTypes.toArray()[0]);
+
+ if (!chosenParty.containsKey(type)) {
+ chosenParty.put(type, card);
+ }
+ } else {
+ multityped.put(card, creatureTypes);
+ }
+ }
+
+ partySize = Math.min(chosenParty.size() + wildcard.size(), 4);
+
+ if (partySize < 4) {
+ partyTypes.removeAll(chosenParty.keySet());
+
+ // Here I'm left with just the party types that I haven't selected.
+ for(Card multi : multityped.keySet()) {
+ Set types = multityped.get(multi);
+ types.retainAll(partyTypes);
+
+ for(String type : types) {
+ chosenParty.put(type, multi);
+ partyTypes.remove(type);
+ break;
+ }
+ }
+ }
+
+ partySize = Math.min(chosenParty.size() + wildcard.size(), 4);
+
+ return doXMath(partySize, expr, c, ctb);
+ }
+
+ // TODO make AI part to understand Sunburst better so this isn't needed
+ if (sq[0].startsWith("UniqueManaColorsProduced")) {
+ boolean untappedOnly = sq[1].contains("ByUntappedSources");
+ int uniqueColors = 0;
+ CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
+ outer: for (byte color : MagicColor.WUBRG) {
+ for (Card card : otb) {
+ if (!card.isTapped() || !untappedOnly) {
+ for (SpellAbility ma : card.getManaAbilities()) {
+ if (ma.canProduce(MagicColor.toShortString(color))) {
+ uniqueColors++;
+ continue outer;
+ }
+ }
+ }
+ }
+ }
+ return doXMath(uniqueColors, expr, c, ctb);
+ }
+
+ // TODO change into checking SpellAbility
+ if (sq[0].contains("xColorPaid")) {
+ String[] attrs = sq[0].split(" ");
+ StringBuilder colors = new StringBuilder();
+ for (int i = 1; i < attrs.length; i++) {
+ colors.append(attrs[i]);
+ }
+ return doXMath(c.getXManaCostPaidCount(colors.toString()), expr, c, ctb);
+ }
+
+ // Count$UrzaLands..
+ if (sq[0].startsWith("UrzaLands")) {
+ return doXMath(AbilityUtils.calculateAmount(c, sq[player.hasUrzaLands() ? 1 : 2], ctb), expr, c, ctb);
+ }
+
+ /////////////////
+ //game info
+ // Count$Morbid..
+ if (sq[0].startsWith("Morbid")) {
+ final List res = CardUtil.getThisTurnEntered(ZoneType.Graveyard, ZoneType.Battlefield, "Creature", c, ctb);
+ return doXMath(calculateAmount(c, sq[res.size() > 0 ? 1 : 2], ctb), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("CreatureType")) {
+ String[] sqparts = l[0].split(" ", 2);
+ final String[] rest = sqparts[1].split(",");
+
+ final CardCollectionView cardsInZones = sqparts[0].length() > 12
+ ? game.getCardsIn(ZoneType.listValueOf(sqparts[0].substring(12)))
+ : game.getCardsIn(ZoneType.Battlefield);
+
+ CardCollection cards = CardLists.getValidCards(cardsInZones, rest, player, c, ctb);
+ final Set creatTypes = Sets.newHashSet();
+
+ for (Card card : cards) {
+ Iterables.addAll(creatTypes, card.getType().getCreatureTypes());
+ }
+ // filter out fun types?
+ int n = creatTypes.contains(CardType.AllCreatureTypes) ? CardType.getAllCreatureTypes().size() : creatTypes.size();
+ return doXMath(n, expr, c, ctb);
+ }
+
+ // Count$Chroma.
+ if (sq[0].startsWith("Chroma")) {
+ ZoneType sourceZone = sq[0].contains("ChromaInGrave") ? ZoneType.Graveyard : ZoneType.Battlefield;
+ final CardCollectionView cards;
+ if (sq[0].contains("ChromaSource")) { // Runs Chroma for passed in Source card
+ cards = new CardCollection(c);
+ }
+ else {
+ cards = player.getCardsIn(sourceZone);
+ }
+
+ int colorOcurrencices = 0;
+ byte colorCode = ManaAtom.fromName(sq[1]);
+ for (Card c0 : cards) {
+ for (ManaCostShard sh : c0.getManaCost()){
+ if (sh.isColor(colorCode))
+ colorOcurrencices++;
+ }
+ }
+ return doXMath(colorOcurrencices, expr, c, ctb);
+ }
+
+ if (l[0].contains("ExactManaCost")) {
+ String[] sqparts = l[0].split(" ", 2);
+ final String[] rest = sqparts[1].split(",");
+
+ final CardCollectionView cardsInZones = sqparts[0].length() > 13
+ ? game.getCardsIn(ZoneType.listValueOf(sqparts[0].substring(13)))
+ : game.getCardsIn(ZoneType.Battlefield);
+
+ CardCollection cards = CardLists.getValidCards(cardsInZones, rest, player, c, ctb);
+ final Set manaCost = Sets.newHashSet();
+
+ for (Card card : cards) {
+ manaCost.add(card.getManaCost().getShortString());
+ }
+
+ return doXMath(manaCost.size(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("StormCount")) {
+ return doXMath(game.getStack().getSpellsCastThisTurn().size() - 1, expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("RolledThisTurn")) {
+ return game.getPhaseHandler().getPlanarDiceRolledthisTurn();
+ }
+
+ if (sq[0].contains("CardTypes")) {
+ return doXMath(getCardTypesFromList(game.getCardsIn(ZoneType.smartValueOf(sq[1]))), expr, c, ctb);
+ }
+
+ if (sq[0].equals("TotalTurns")) {
+ return doXMath(game.getPhaseHandler().getTurn(), expr, c, ctb);
+ }
+
+ if (sq[0].equals("MaxDistinctOnStack")) {
+ return doXMath(game.getStack().getMaxDistinctSources(), expr, c, ctb);
+ }
+
+ //Count$Random..
+ if (sq[0].equals("Random")) {
+ int min = AbilityUtils.calculateAmount(c, sq[1], ctb);
+ int max = AbilityUtils.calculateAmount(c, sq[2], ctb);
+
+ return MyRandom.getRandom().nextInt(1+max-min) + min;
+ }
+
+ // Count$SumPower_valid
+ if (sq[0].startsWith("SumPower")) {
+ final String[] restrictions = l[0].split("_");
+ final String[] rest = restrictions[1].split(",");
+ CardCollection filteredCards = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
+ return doXMath(Aggregates.sum(filteredCards, CardPredicates.Accessors.fnGetNetPower), expr, c, ctb);
+ }
+
+ // Count$SumCMC_valid
+ if (sq[0].startsWith("SumCMC")) {
+ ZoneType zone = ZoneType.Battlefield;
+ //graveyard support for Inferno Project (may need other zones or multi-zone in future)
+ if (sq[0].contains("Graveyard"))
+ zone = ZoneType.Graveyard;
+ final String[] restrictions = l[0].split("_");
+ final String[] rest = restrictions[1].split(",");
+ CardCollectionView cardsonbattlefield = game.getCardsIn(zone);
+ CardCollection filteredCards = CardLists.getValidCards(cardsonbattlefield, rest, player, c, ctb);
+ return Aggregates.sum(filteredCards, CardPredicates.Accessors.fnGetCmc);
+ }
+
+ // Count$TotalCounters._
+ if (sq[0].startsWith("TotalCounters")) {
+ final String[] restrictions = l[0].split("_");
+ final CounterType cType = CounterType.getType(restrictions[1]);
+ final String[] validFilter = restrictions[2].split(",");
+ CardCollectionView validCards = game.getCardsIn(ZoneType.Battlefield);
+ validCards = CardLists.getValidCards(validCards, validFilter, player, c, ctb);
+ int cCount = 0;
+ for (final Card card : validCards) {
+ cCount += card.getCounters(cType);
+ }
+ return doXMath(cCount, expr, c, ctb);
+ }
+
+ // Count$ThisTurnCast
+ // Count$LastTurnCast
+ if (sq[0].startsWith("ThisTurnCast") || sq[0].startsWith("LastTurnCast")) {
+
+ final String[] workingCopy = l[0].split("_");
+ final String validFilter = workingCopy[1];
+
+ List res = Lists.newArrayList();
+ if (workingCopy[0].contains("This")) {
+ res = CardUtil.getThisTurnCast(validFilter, c, ctb);
+ } else {
+ res = CardUtil.getLastTurnCast(validFilter, c, ctb);
+ }
+
+ return doXMath(res.size(), expr, c, ctb);
+ }
+
+ // Count$ThisTurnEntered [from ]
+ if (sq[0].startsWith("ThisTurnEntered")) {
+ final String[] workingCopy = l[0].split("_");
+
+ ZoneType destination = ZoneType.smartValueOf(workingCopy[1]);
+ final boolean hasFrom = workingCopy[2].equals("from");
+ ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null;
+ String validFilter = workingCopy[hasFrom ? 4 : 2] ;
+
+ final List res = CardUtil.getThisTurnEntered(destination, origin, validFilter, c, ctb);
+ if (origin == null) { // Remove cards on the battlefield that changed controller
+ res.removeAll(CardUtil.getThisTurnEntered(destination, destination, validFilter, c, ctb));
+ }
+ return doXMath(res.size(), expr, c, ctb);
+ }
+
+ // Count$LastTurnEntered [from ]
+ if (sq[0].startsWith("LastTurnEntered")) {
+ final String[] workingCopy = l[0].split("_");
+
+ ZoneType destination = ZoneType.smartValueOf(workingCopy[1]);
+ final boolean hasFrom = workingCopy[2].equals("from");
+ ZoneType origin = hasFrom ? ZoneType.smartValueOf(workingCopy[3]) : null;
+ String validFilter = workingCopy[hasFrom ? 4 : 2] ;
+
+ final List res = CardUtil.getLastTurnEntered(destination, origin, validFilter, c, ctb);
+ if (origin == null) { // Remove cards on the battlefield that changed controller
+ res.removeAll(CardUtil.getLastTurnEntered(destination, destination, validFilter, c, ctb));
+ }
+ return doXMath(res.size(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("CountersAddedThisTurn")) {
+ final String[] parts = l[0].split(" ");
+ CounterType cType = CounterType.getType(parts[1]);
+
+ return doXMath(game.getCounterAddedThisTurn(cType, parts[2], parts[3], c, player, ctb), expr, c, ctb);
+ }
+
+ // count valid cards in any specified zone/s
+ if (sq[0].startsWith("Valid")) {
+ String[] lparts = l[0].split(" ", 2);
+
+ final CardCollectionView cardsInZones = lparts[0].length() > 5
+ ? game.getCardsIn(ZoneType.listValueOf(lparts[0].substring(5)))
+ : game.getCardsIn(ZoneType.Battlefield);
+
+ return doXMath(CardLists.getValidCardCount(cardsInZones, lparts[1], player, c, ctb), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("GreatestPower")) {
+ final String[] lparts = l[0].split("_", 2);
+ final String[] rest = lparts[1].split(",");
+ final CardCollectionView cardsInZones = lparts[0].length() > 13
+ ? game.getCardsIn(ZoneType.listValueOf(lparts[0].substring(13)))
+ : game.getCardsIn(ZoneType.Battlefield);
+ CardCollection list = CardLists.getValidCards(cardsInZones, rest, player, c, ctb);
+ int highest = 0;
+ for (final Card crd : list) {
+ if (crd.getNetPower() > highest) {
+ highest = crd.getNetPower();
+ }
+ }
+ return highest;
+ }
+
+ if (sq[0].startsWith("GreatestToughness_")) {
+ final String restriction = l[0].substring(18);
+ final String[] rest = restriction.split(",");
+ CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
+ int highest = 0;
+ for (final Card crd : list) {
+ if (crd.getNetToughness() > highest) {
+ highest = crd.getNetToughness();
+ }
+ }
+ return highest;
+ }
+
+ if (sq[0].startsWith("HighestCMC_")) {
+ final String restriction = l[0].substring(11);
+ final String[] rest = restriction.split(",");
+ CardCollection list = CardLists.getValidCards(game.getCardsInGame(), rest, player, c, ctb);
+ int highest = 0;
+ for (final Card crd : list) {
+ // dont check for Split card anymore
+ if (crd.getCMC() > highest) {
+ highest = crd.getCMC();
+ }
+ }
+ return highest;
+ }
+
+ if (sq[0].startsWith("MostCardName")) {
+ String[] lparts = l[0].split(" ", 2);
+ final String[] rest = lparts[1].split(",");
+
+ final CardCollectionView cardsInZones = lparts[0].length() > 12
+ ? game.getCardsIn(ZoneType.listValueOf(lparts[0].substring(12)))
+ : game.getCardsIn(ZoneType.Battlefield);
+
+ CardCollection cards = CardLists.getValidCards(cardsInZones, rest, player, c, ctb);
+ final Map map = Maps.newHashMap();
+ for (final Card card : cards) {
+ // Remove Duplicated types
+ final String name = card.getName();
+ Integer count = map.get(name);
+ map.put(name, count == null ? 1 : count + 1);
+ }
+ int max = 0;
+ for (final Entry entry : map.entrySet()) {
+ if (max < entry.getValue()) {
+ max = entry.getValue();
+ }
+ }
+ return max;
+ }
+
+ if (sq[0].startsWith("DifferentCardNames_")) {
+ final List crdname = Lists.newArrayList();
+ final String restriction = l[0].substring(19);
+ final String[] rest = restriction.split(",");
+ CardCollection list = CardLists.getValidCards(game.getCardsInGame(), rest, player, c, ctb);
+ for (final Card card : list) {
+ String name = card.getName();
+ // CR 201.2b Those objects have different names only if each of them has at least one name and no two objects in that group have a name in common
+ if (!crdname.contains(name) && !name.isEmpty()) {
+ crdname.add(name);
+ }
+ }
+ return doXMath(crdname.size(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("DifferentPower_")) {
+ final List powers = Lists.newArrayList();
+ final String restriction = l[0].substring(15);
+ final String[] rest = restriction.split(",");
+ CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
+ for (final Card card : list) {
+ Integer pow = card.getNetPower();
+ if (!powers.contains(pow)) {
+ powers.add(pow);
+ }
+ }
+ 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);
+ return doXMath(CardFactoryUtil.getMostProminentCreatureTypeSize(list), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("SecondMostProminentColor")) {
+ String restriction = l[0].split(" ")[1];
+ CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), restriction, player, c, ctb);
+ int[] colorSize = CardFactoryUtil.SortColorsFromList(list);
+ return doXMath(colorSize[colorSize.length - 2], expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("ColorsCtrl")) {
+ final String restriction = l[0].substring(11);
+ final String[] rest = restriction.split(",");
+ final CardCollection list = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), rest, player, c, ctb);
+ byte n = 0;
+ for (final Card card : list) {
+ n |= card.determineColor().getColor();
+ }
+ return doXMath(ColorSet.fromMask(n).countColors(), expr, c, ctb);
+ }
+
+ if (sq[0].startsWith("CreatureType")) {
+ String[] sqparts = l[0].split(" ", 2);
+ final String[] rest = sqparts[1].split(",");
+
+ final CardCollectionView cardsInZones = sqparts[0].length() > 12
+ ? game.getCardsIn(ZoneType.listValueOf(sqparts[0].substring(12)))
+ : game.getCardsIn(ZoneType.Battlefield);
+
+ CardCollection cards = CardLists.getValidCards(cardsInZones, rest, player, c, ctb);
+ final Set creatTypes = Sets.newHashSet();
+
+ for (Card card : cards) {
+ Iterables.addAll(creatTypes, card.getType().getCreatureTypes());
+ }
+ int n = creatTypes.contains(CardType.AllCreatureTypes) ? CardType.getAllCreatureTypes().size() : creatTypes.size();
+ return doXMath(n, expr, c, ctb);
+ }
+ // Complex counting methods
+ CardCollectionView someCards = getCardListForXCount(c, player, sq, ctb);
+
+ // 1/10 - Count$MaxCMCYouCtrl
+ if (sq[0].startsWith("MaxCMC")) {
+ int mmc = Aggregates.max(someCards, CardPredicates.Accessors.fnGetCmc);
+ return doXMath(mmc, expr, c, ctb);
+ }
+
+ return doXMath(someCards.size(), expr, c, ctb);
}
public static final void applyManaColorConversion(ManaConversionMatrix matrix, final Map params) {
@@ -2134,4 +3025,648 @@ public class AbilityUtils {
sa.setDescription(sa.getDescription() + " (Splicing " + c + " onto it)");
sa.addSplicedCards(c);
}
+
+ public static int doXMath(final int num, final String operators, final Card c, CardTraitBase ctb) {
+ if (operators == null || operators.equals("none")) {
+ return num;
+ }
+
+ final String[] s = operators.split("\\.");
+ int secondaryNum = 0;
+
+ try {
+ if (s.length == 2) {
+ secondaryNum = Integer.parseInt(s[1]);
+ }
+ } catch (final Exception e) {
+ secondaryNum = AbilityUtils.calculateAmount(c, s[1], ctb);
+ }
+
+ if (s[0].contains("Plus")) {
+ return num + secondaryNum;
+ } else if (s[0].contains("NMinus")) {
+ return secondaryNum - num;
+ } else if (s[0].contains("Minus")) {
+ return num - secondaryNum;
+ } else if (s[0].contains("Twice")) {
+ return num * 2;
+ } else if (s[0].contains("Thrice")) {
+ return num * 3;
+ } else if (s[0].contains("HalfUp")) {
+ return (int) (Math.ceil(num / 2.0));
+ } else if (s[0].contains("HalfDown")) {
+ return (int) (Math.floor(num / 2.0));
+ } else if (s[0].contains("ThirdUp")) {
+ return (int) (Math.ceil(num / 3.0));
+ } else if (s[0].contains("ThirdDown")) {
+ return (int) (Math.floor(num / 3.0));
+ } else if (s[0].contains("Negative")) {
+ return num * -1;
+ } else if (s[0].contains("Times")) {
+ return num * secondaryNum;
+ } else if (s[0].contains("DivideEvenlyDown")) {
+ if (secondaryNum == 0) {
+ return 0;
+ } else {
+ return num / secondaryNum;
+ }
+ } else if (s[0].contains("Mod")) {
+ return num % secondaryNum;
+ } else if (s[0].contains("Abs")) {
+ return Math.abs(num);
+ } else if (s[0].contains("LimitMax")) {
+ if (num < secondaryNum) {
+ return num;
+ } else {
+ return secondaryNum;
+ }
+ } else if (s[0].contains("LimitMin")) {
+ if (num > secondaryNum) {
+ return num;
+ } else {
+ return secondaryNum;
+ }
+
+ } else {
+ return num;
+ }
+ }
+
+ /**
+ *
+ * Parse player targeted X variables.
+ *
+ *
+ * @param players
+ * a {@link java.util.ArrayList} object.
+ * @param s
+ * a {@link java.lang.String} object.
+ * @param source
+ * a {@link forge.game.card.Card} object.
+ * @return a int.
+ */
+ public static int playerXCount(final List players, final String s, final Card source, CardTraitBase ctb) {
+ if (players.size() == 0) {
+ return 0;
+ }
+
+ final String[] l = s.split("/");
+ final String m = CardFactoryUtil.extractOperators(s);
+
+ int n = 0;
+
+ if (l[0].startsWith("TotalCommanderCastFromCommandZone")) {
+ int totCast = 0;
+ for (Player p : players) {
+ totCast += p.getTotalCommanderCast();
+ }
+ return doXMath(totCast, m, source, ctb);
+ }
+
+ // methods for getting the highest/lowest playerXCount from a range of players
+ if (l[0].startsWith("Highest")) {
+ for (final Player player : players) {
+ final int current = playerXProperty(player, TextUtil.fastReplace(s, "Highest", ""), source, ctb);
+ if (current > n) {
+ n = current;
+ }
+ }
+
+ return doXMath(n, m, source, ctb);
+ }
+
+ if (l[0].startsWith("Lowest")) {
+ n = 99999; // if no players have fewer than 99999 valids, the game is frozen anyway
+ for (final Player player : players) {
+ final int current = playerXProperty(player, TextUtil.fastReplace(s, "Lowest", ""), source, ctb);
+ if (current < n) {
+ n = current;
+ }
+ }
+ return doXMath(n, m, source, ctb);
+ }
+
+ if (l[0].startsWith("TiedForHighestLife")) {
+ int maxLife = Integer.MIN_VALUE;
+ for (final Player player : players) {
+ int highestTotal = playerXProperty(player, "LifeTotal", source, ctb);
+ if (highestTotal > maxLife) {
+ maxLife = highestTotal;
+ }
+ }
+ int numTied = 0;
+ for (final Player player : players) {
+ if (player.getLife() == maxLife) {
+ numTied++;
+ }
+ }
+ return doXMath(numTied, m, source, ctb);
+ }
+
+ if (l[0].startsWith("TiedForLowestLife")) {
+ int minLife = Integer.MAX_VALUE;
+ for (final Player player : players) {
+ int lowestTotal = playerXProperty(player, "LifeTotal", source, ctb);
+ if (lowestTotal < minLife) {
+ minLife = lowestTotal;
+ }
+ }
+ int numTied = 0;
+ for (final Player player : players) {
+ if (player.getLife() == minLife) {
+ numTied++;
+ }
+ }
+ return doXMath(numTied, m, source, ctb);
+ }
+
+ final String[] sq;
+ sq = l[0].split("\\.");
+
+ // the number of players passed in
+ if (sq[0].equals("Amount")) {
+ return doXMath(players.size(), m, source, ctb);
+ }
+
+ if (sq[0].startsWith("HasProperty")) {
+ int totPlayer = 0;
+ String property = sq[0].substring(11);
+ for (Player p : players) {
+ if (p.hasProperty(property, source.getController(), source, ctb)) {
+ totPlayer++;
+ }
+ }
+ return doXMath(totPlayer, m, source, ctb);
+ }
+
+ if (sq[0].contains("DamageThisTurn")) {
+ int totDmg = 0;
+ for (Player p : players) {
+ totDmg += p.getAssignedDamage();
+ }
+ return doXMath(totDmg, m, source, ctb);
+ } else if (sq[0].contains("LifeLostThisTurn")) {
+ int totDmg = 0;
+ for (Player p : players) {
+ totDmg += p.getLifeLostThisTurn();
+ }
+ return doXMath(totDmg, m, source, ctb);
+ }
+
+ if (players.size() > 0) {
+ int totCount = 0;
+ for (Player p : players) {
+ totCount += playerXProperty(p, s, source, ctb);
+ }
+ return totCount;
+ }
+
+ return doXMath(n, m, source, ctb);
+ }
+
+ public static int playerXProperty(final Player player, final String s, final Card source, CardTraitBase ctb) {
+ final String[] l = s.split("/");
+ final String m = CardFactoryUtil.extractOperators(s);
+
+ final Game game = player.getGame();
+
+ // count valid cards in any specified zone/s
+ if (l[0].startsWith("Valid") && !l[0].contains("Valid ")) {
+ String[] lparts = l[0].split(" ", 2);
+ final List vZone = ZoneType.listValueOf(lparts[0].split("Valid")[1]);
+ String restrictions = TextUtil.fastReplace(l[0], TextUtil.addSuffix(lparts[0]," "), "");
+ final String[] rest = restrictions.split(",");
+ CardCollection cards = CardLists.getValidCards(game.getCardsIn(vZone), rest, player, source, ctb);
+ return doXMath(cards.size(), m, source, ctb);
+ }
+
+ // count valid cards on the battlefield
+ if (l[0].startsWith("Valid ")) {
+ final String restrictions = l[0].substring(6);
+ final String[] rest = restrictions.split(",");
+ CardCollection cardsonbattlefield = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), rest, player, source, ctb);
+ return doXMath(cardsonbattlefield.size(), m, source, ctb);
+ }
+
+ final String[] sq = l[0].split("\\.");
+ final String value = sq[0];
+
+ if (value.contains("CardsInHand")) {
+ return doXMath(player.getCardsIn(ZoneType.Hand).size(), m, source, ctb);
+ }
+
+ if (value.contains("NumPowerSurgeLands")) {
+ return doXMath(player.getNumPowerSurgeLands(), m, source, ctb);
+ }
+
+ if (value.contains("DomainPlayer")) {
+ int n = 0;
+ final CardCollectionView someCards = player.getCardsIn(ZoneType.Battlefield);
+ final List basic = MagicColor.Constant.BASIC_LANDS;
+
+ for (int i = 0; i < basic.size(); i++) {
+ if (!CardLists.getType(someCards, basic.get(i)).isEmpty()) {
+ n++;
+ }
+ }
+ return doXMath(n, m, source, ctb);
+ }
+
+ if (value.contains("CardsInLibrary")) {
+ return doXMath(player.getCardsIn(ZoneType.Library).size(), m, source, ctb);
+ }
+
+ if (value.contains("CardsInGraveyard")) {
+ return doXMath(player.getCardsIn(ZoneType.Graveyard).size(), m, source, ctb);
+ }
+ if (value.contains("LandsInGraveyard")) {
+ return doXMath(CardLists.getType(player.getCardsIn(ZoneType.Graveyard), "Land").size(), m, source, ctb);
+ }
+
+ if (value.contains("CreaturesInPlay")) {
+ return doXMath(player.getCreaturesInPlay().size(), m, source, ctb);
+ }
+
+ if (value.contains("CardsInPlay")) {
+ return doXMath(player.getCardsIn(ZoneType.Battlefield).size(), m, source, ctb);
+ }
+
+ if (value.contains("StartingLife")) {
+ return doXMath(player.getStartingLife(), m, source, ctb);
+ }
+
+ if (value.contains("LifeTotal")) {
+ return doXMath(player.getLife(), m, source, ctb);
+ }
+
+ if (value.contains("LifeLostThisTurn")) {
+ return doXMath(player.getLifeLostThisTurn(), m, source, ctb);
+ }
+
+ if (value.contains("LifeLostLastTurn")) {
+ return doXMath(player.getLifeLostLastTurn(), m, source, ctb);
+ }
+
+ if (value.contains("LifeGainedThisTurn")) {
+ return doXMath(player.getLifeGainedThisTurn(), m, source, ctb);
+ }
+
+ if (value.contains("LifeGainedByTeamThisTurn")) {
+ return doXMath(player.getLifeGainedByTeamThisTurn(), m, source, ctb);
+ }
+
+ if (value.contains("LifeStartedThisTurnWith")) {
+ return doXMath(player.getLifeStartedThisTurnWith(), m, source, ctb);
+ }
+
+ if (value.contains("PoisonCounters")) {
+ return doXMath(player.getPoisonCounters(), m, source, ctb);
+ }
+
+ if (value.contains("TopOfLibraryCMC")) {
+ return doXMath(Aggregates.sum(player.getCardsIn(ZoneType.Library, 1), CardPredicates.Accessors.fnGetCmc), m, source, ctb);
+ }
+
+ if (value.contains("LandsPlayed")) {
+ return doXMath(player.getLandsPlayedThisTurn(), m, source, ctb);
+ }
+
+ if (value.contains("CardsDrawn")) {
+ return doXMath(player.getNumDrawnThisTurn(), m, source, ctb);
+ }
+
+ if (value.contains("CardsDiscardedThisTurn")) {
+ return doXMath(player.getNumDiscardedThisTurn(), m, source, ctb);
+ }
+
+ if (value.contains("TokensCreatedThisTurn")) {
+ return doXMath(player.getNumTokensCreatedThisTurn(), m, source, ctb);
+ }
+
+ if (value.contains("AttackersDeclared")) {
+ return doXMath(player.getAttackersDeclaredThisTurn(), m, source, ctb);
+ }
+
+ if (value.contains("DamageToOppsThisTurn")) {
+ int oppDmg = 0;
+ for (Player opp : player.getOpponents()) {
+ oppDmg += opp.getAssignedDamage();
+ }
+ return doXMath(oppDmg, m, source, ctb);
+ }
+
+ if (value.contains("NonCombatDamageDealtThisTurn")) {
+ return doXMath(player.getAssignedDamage() - player.getAssignedCombatDamage(), m, source, ctb);
+ }
+
+ if (value.equals("OpponentsAttackedThisTurn")) {
+ final PlayerCollection opps = game.getPlayersAttackedThisTurn().get(player);
+ return doXMath(opps == null ? 0 : opps.size(), m, source, ctb);
+ }
+
+ return doXMath(0, m, source, ctb);
+ }
+
+ /**
+ *
+ * Parse player targeted X variables.
+ *
+ *
+ * @param objects
+ * a {@link java.util.ArrayList} object.
+ * @param s
+ * a {@link java.lang.String} object.
+ * @param source
+ * a {@link forge.game.card.Card} object.
+ * @return a int.
+ */
+ public static int objectXCount(final List> objects, final String s, final Card source, CardTraitBase ctb) {
+ if (objects.isEmpty()) {
+ return 0;
+ }
+
+ if (s.startsWith("Valid")) {
+ return handlePaid(Iterables.filter(objects, Card.class), s, source, ctb);
+ }
+
+ int n = s.startsWith("Amount") ? objects.size() : 0;
+ return doXMath(n, CardFactoryUtil.extractOperators(s), source, ctb);
+ }
+
+
+ /**
+ *
+ * handlePaid.
+ *
+ *
+ * @param paidList
+ * a {@link forge.game.card.CardCollectionView} object.
+ * @param string
+ * a {@link java.lang.String} object.
+ * @param source
+ * a {@link forge.game.card.Card} object.
+ * @return a int.
+ */
+ public static int handlePaid(final Iterable paidList, final String string, final Card source, CardTraitBase ctb) {
+ if (paidList == null) {
+ if (string.contains(".")) {
+ final String[] splitString = string.split("\\.", 2);
+ return doXMath(0, splitString[1], source, ctb);
+ } else {
+ return 0;
+ }
+ }
+ if (string.startsWith("Amount")) {
+ int size = Iterables.size(paidList);
+ if (string.contains(".")) {
+ final String[] splitString = string.split("\\.", 2);
+ return doXMath(size, splitString[1], source, ctb);
+ } else {
+ return size;
+ }
+
+ }
+
+ if (string.startsWith("DifferentCMC")) {
+ final Set diffCMC = new HashSet<>();
+ for (final Card card : paidList) {
+ diffCMC.add(card.getCMC());
+ }
+ return diffCMC.size();
+ }
+
+ if (string.startsWith("SumCMC")) {
+ int sumCMC = 0;
+ for(Card c : paidList) {
+ sumCMC += c.getCMC();
+ }
+ return sumCMC;
+ }
+
+ if (string.startsWith("Valid")) {
+
+ final String[] splitString = string.split("/", 2);
+ String valid = splitString[0].substring(6);
+ final List list = CardLists.getValidCardsAsList(paidList, valid, source.getController(), source, ctb);
+ return doXMath(list.size(), splitString.length > 1 ? splitString[1] : null, source, ctb);
+ }
+
+ String filteredString = string;
+ Iterable filteredList = paidList;
+ final String[] filter = filteredString.split("_");
+
+ if (string.startsWith("FilterControlledBy")) {
+ final String pString = filter[0].substring(18);
+ FCollectionView controllers = AbilityUtils.getDefinedPlayers(source, pString, ctb);
+ filteredList = CardLists.filterControlledByAsList(filteredList, controllers);
+ filteredString = TextUtil.fastReplace(filteredString, pString, "");
+ filteredString = TextUtil.fastReplace(filteredString, "FilterControlledBy_", "");
+ }
+
+ int tot = 0;
+ for (final Card c : filteredList) {
+ tot += xCount(c, filteredString, ctb);
+ }
+
+ return tot;
+ }
+
+
+ private static CardCollectionView getCardListForXCount(final Card c, final Player cc, final String[] sq, CardTraitBase ctb) {
+ final List opps = cc.getOpponents();
+ CardCollection someCards = new CardCollection();
+ final Game game = c.getGame();
+
+ // Generic Zone-based counting
+ // Count$QualityAndZones.Subquality
+
+ // build a list of cards in each possible specified zone
+
+ if (sq[0].contains("YouCtrl")) {
+ someCards.addAll(cc.getCardsIn(ZoneType.Battlefield));
+ }
+
+ if (sq[0].contains("InYourYard")) {
+ someCards.addAll(cc.getCardsIn(ZoneType.Graveyard));
+ }
+
+ if (sq[0].contains("InYourLibrary")) {
+ someCards.addAll(cc.getCardsIn(ZoneType.Library));
+ }
+
+ if (sq[0].contains("InYourHand")) {
+ someCards.addAll(cc.getCardsIn(ZoneType.Hand));
+ }
+
+ if (sq[0].contains("InYourSideboard")) {
+ someCards.addAll(cc.getCardsIn(ZoneType.Sideboard));
+ }
+
+ if (sq[0].contains("OppCtrl")) {
+ for (final Player p : opps) {
+ someCards.addAll(p.getZone(ZoneType.Battlefield).getCards());
+ }
+ }
+
+ if (sq[0].contains("InOppYard")) {
+ for (final Player p : opps) {
+ someCards.addAll(p.getCardsIn(ZoneType.Graveyard));
+ }
+ }
+
+ if (sq[0].contains("InOppHand")) {
+ for (final Player p : opps) {
+ someCards.addAll(p.getCardsIn(ZoneType.Hand));
+ }
+ }
+
+ if (sq[0].contains("InChosenHand")) {
+ if (c.getChosenPlayer() != null) {
+ someCards.addAll(c.getChosenPlayer().getCardsIn(ZoneType.Hand));
+ }
+ }
+
+ if (sq[0].contains("InRememberedHand")) {
+ if (c.getRemembered() != null) {
+ for (final Object o : c.getRemembered()) {
+ if (o instanceof Player) {
+ Player remPlayer = (Player) o;
+ someCards.addAll(remPlayer.getCardsIn(ZoneType.Hand));
+ }
+ }
+ }
+ }
+
+ if (sq[0].contains("InChosenYard")) {
+ if (c.getChosenPlayer() != null) {
+ someCards.addAll(c.getChosenPlayer().getCardsIn(ZoneType.Graveyard));
+ }
+ }
+
+ if (sq[0].contains("OnBattlefield")) {
+ someCards.addAll(game.getCardsIn(ZoneType.Battlefield));
+ }
+
+ if (sq[0].contains("InAllYards")) {
+ someCards.addAll(game.getCardsIn(ZoneType.Graveyard));
+ }
+
+ if (sq[0].contains("SpellsOnStack")) {
+ someCards.addAll(game.getCardsIn(ZoneType.Stack));
+ }
+
+ if (sq[0].contains("InAllHands")) {
+ someCards.addAll(game.getCardsIn(ZoneType.Hand));
+ }
+
+ // Count$InTargetedHand (targeted player's cards in hand)
+ if (sq[0].contains("InTargetedHand")) {
+ for (Player tgtP : AbilityUtils.getDefinedPlayers(c, "TargetedPlayer", ctb)) {
+ someCards.addAll(tgtP.getCardsIn(ZoneType.Hand));
+ }
+ }
+
+ // Count$InTargetedHand (targeted player's cards in hand)
+ if (sq[0].contains("InTargetedLibrary")) {
+ for (Player tgtP : AbilityUtils.getDefinedPlayers(c, "TargetedPlayer", ctb)) {
+ someCards.addAll(tgtP.getCardsIn(ZoneType.Library));
+ }
+ }
+
+ // Count$InTargetedHand (targeted player's cards in hand)
+ if (sq[0].contains("InEnchantedHand")) {
+ GameEntity o = c.getEntityAttachedTo();
+ Player controller = null;
+ if (o instanceof Card) {
+ controller = ((Card) o).getController();
+ }
+ else {
+ controller = (Player) o;
+ }
+ if (controller != null) {
+ someCards.addAll(controller.getCardsIn(ZoneType.Hand));
+ }
+ }
+ if (sq[0].contains("InEnchantedYard")) {
+ GameEntity o = c.getEntityAttachedTo();
+ Player controller = null;
+ if (o instanceof Card) {
+ controller = ((Card) o).getController();
+ }
+ else {
+ controller = (Player) o;
+ }
+ if (controller != null) {
+ someCards.addAll(controller.getCardsIn(ZoneType.Graveyard));
+ }
+ }
+
+ // filter lists based on the specified quality
+
+ // "Clerics you control" - Count$TypeYouCtrl.Cleric
+ if (sq[0].contains("Type")) {
+ someCards = CardLists.filter(someCards, CardPredicates.isType(sq[1]));
+ }
+
+ // "Named in all graveyards" - Count$NamedAllYards.