Merge branch 'master' of git.cardforge.org:core-developers/forge into agetian-master

This commit is contained in:
Michael Kamensky
2021-06-05 17:34:06 +03:00
1889 changed files with 19680 additions and 14391 deletions

2
.gitignore vendored
View File

@@ -32,6 +32,8 @@ bin
gen
*.log
# Ignore macOS Spotlight rubbish
.DS_Store
# TODO: specify what these ignores are for (releasing?)

View File

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

View File

@@ -54,7 +54,6 @@ import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
/**
* <p>
* 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<String> 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
}

View File

@@ -180,11 +180,9 @@ public class AiBlockController {
// Good Blocks means a good trade or no trade
private void makeGoodBlocks(final Combat combat) {
List<Card> 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<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
final List<Card> safeBlockers = getSafeBlockers(combat, attacker, blockers);
@@ -305,7 +302,6 @@ public class AiBlockController {
}
Card blocker = null;
final List<Card> 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<Card> currentAttackers = new ArrayList<>(attackersLeft);
List<Card> 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<Card> currentAttackers = new ArrayList<>(attackersLeft);
makeChumpBlocks(combat, currentAttackers);
@@ -639,7 +629,6 @@ public class AiBlockController {
}
private void makeChumpBlocks(final Combat combat, List<Card> 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<Card> 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<Card> chumpBlockers;
List<Card> 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<Card> safeBlockers;
List<Card> blockers;
List<Card> 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)) {

View File

@@ -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<SpellAbility> 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<SpellAbility> 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<AbilityKey, Object> 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<SpellAbility> singleSpellAbilityList(SpellAbility sa) {
if (sa == null) { return null; }
// System.out.println("Chosen to play: " + sa);
final List<SpellAbility> 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<SpellAbility> 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);
}
}

View File

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

View File

@@ -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<Card> 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<Card> 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<Card> EvaluateCreatureComparator = new Comparator<Card>() {
@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<Deck> AI_KNOWS_HOW_TO_PLAY_ALL_CARDS = new Predicate<Deck>() {
@Override
public boolean apply(Deck d) {
for(Entry<DeckSection, CardPool> cp: d) {
for(Entry<PaperCard, Integer> e : cp.getValue()) {
if ( e.getKey().getRules().getAiHints().getRemAIDecks() )
for (Entry<DeckSection, CardPool> cp: d) {
for (Entry<PaperCard, Integer> 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<KeywordInterface> 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<String, String> 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());
}

View File

@@ -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<Card> 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
/**
* <p>
* 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<Card> 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<Card> plannedAttackers) {
final Game game = attacker.getGame();
final Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(
ReplacementType.DamageDone, repParams, ReplacementLayer.Other);
return !list.isEmpty();
for (final ReplacementEffect re : list) {
Map<String, String> 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<Card> categorizeAttackersByEvasion(List<Card> attackers) {
List<Card> categorizedAttackers = Lists.newArrayList();
@@ -2602,5 +2551,3 @@ public class ComputerUtilCombat {
return false;
}
}

View File

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

View File

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

View File

@@ -162,9 +162,9 @@ public class CreatureEvaluator implements Function<Card, Integer> {
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");

View File

@@ -1215,8 +1215,7 @@ public abstract class GameState {
boolean tapped = c.isTapped();
boolean sickness = c.hasSickness();
Map<CounterType, Integer> 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

View File

@@ -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<GameEntity, Integer> divideShield(Card effectSource, Map<GameEntity, Integer> 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<SpellAbility> 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<String, Object> 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<Player, PaperCard> 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)) ||

View File

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

View File

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

View File

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

View File

@@ -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<Card, Integer> 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;
}

View File

@@ -555,12 +555,7 @@ public class AttachAi extends SpellAbilityAi {
if (!evenBetterList.isEmpty()) {
betterList = evenBetterList;
}
evenBetterList = CardLists.filter(betterList, new Predicate<Card>() {
@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<Card> 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<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
@@ -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);
}
});

View File

@@ -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;
}
/**
* <p>
* 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<Player, Map.Entry<String, Integer>> max = Collections.max(data.entrySet(), new Comparator<Map.Entry<Player, Map.Entry<String, Integer>>>() {
@Override
public int compare(Map.Entry<Player, Map.Entry<String, Integer>> o1, Map.Entry<Player, Map.Entry<String, Integer>> o2) {
return o1.getValue().getValue() - o2.getValue().getValue();
if (!data.isEmpty()) {
// JAVA 1.8 use Map.Entry.comparingByValue() somehow
Map.Entry<Player, Map.Entry<String, Integer>> max = Collections.max(data.entrySet(), new Comparator<Map.Entry<Player, Map.Entry<String, Integer>>>() {
@Override
public int compare(Map.Entry<Player, Map.Entry<String, Integer>> o1, Map.Entry<Player, Map.Entry<String, Integer>> 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);
}

View File

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

View File

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

View File

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

View File

@@ -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<Card>() {
@Override
public boolean apply(final Card c) {

View File

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

View File

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

View File

@@ -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<Card>() {
@Override

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
*/

View File

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

View File

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

View File

@@ -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<Card> comp) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
for (final Card c : comp) {

View File

@@ -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<Card> 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<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
final List<Card> 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()) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Card> human = CardLists.filter(validTappables, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getController().equals(opp);
}
});
final List<Card> compy = CardLists.filter(validTappables, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getController().equals(ai);
}
});
final List<Card> human = CardLists.filterControlledBy(validTappables, opp);
final List<Card> 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;
}

View File

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

View File

@@ -66,7 +66,7 @@ public class UntapAi extends SpellAbilityAi {
if (!sa.usesTargeting()) {
final List<Card> pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai;
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<Card> 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;

View File

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

View File

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

View File

@@ -151,6 +151,11 @@ public class StaticData {
return this.editions;
}
public final CardEdition.Collection getCustomEditions(){
return this.customEditions;
}
private List<CardEdition> sortedEditions;
public final List<CardEdition> 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;

View File

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

View File

@@ -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<PaperCard> cards = getAllCards(cardName);
if (null == cards) {
@@ -628,6 +634,23 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}));
}
public Collection<PaperCard> getAllNonPromosNonReprintsNoAlt() {
return Lists.newArrayList(Iterables.filter(getAllCardsNoAlt(), new Predicate<PaperCard>() {
@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();

View File

@@ -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<CardEdition> { // 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<CardEdition> { // immutable
}
}
public static class CardInSet {
public static class CardInSet implements Comparable<CardInSet> {
public final CardRarity rarity;
public final String collectorNumber;
public final String name;
@@ -172,6 +174,56 @@ public final class CardEdition implements Comparable<CardEdition> { // 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<CardEdition> { // immutable
private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, CardInSet> cardMap;
private final List<CardInSet> cardsInSet;
private final Map<String, Integer> tokenNormalized;
// custom print sheets that will be loaded lazily
private final Map<String, List<String>> customPrintSheetsToParse;
@@ -215,13 +268,18 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
private CardEdition(ListMultimap<String, CardInSet> cardMap, Map<String, Integer> tokens, Map<String, List<String>> customPrintSheetsToParse) {
this.cardMap = cardMap;
this.cardsInSet = new ArrayList<>(cardMap.values());
Collections.sort(cardsInSet);
this.tokenNormalized = tokens;
this.customPrintSheetsToParse = customPrintSheetsToParse;
}
private CardEdition(CardInSet[] cards, Map<String, Integer> tokens) {
List<CardInSet> 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<CardEdition> { // 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<CardEdition> { // immutable
public List<CardInSet> getCards() { return cardMap.get("cards"); }
public List<CardInSet> 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<CardEdition> { // 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<CardEdition> { // 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<CardEdition> { // immutable
* rarity - grouping #4
* name - grouping #5
*/
"(^([0-9]+.?) )?(([SCURML]) )?(.*)$"
"(^([0-9A-Z]+.?) )?(([SCURML]) )?(.*)$"
);
ListMultimap<String, CardInSet> cardMap = ArrayListMultimap.create();

View File

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

View File

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

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
@@ -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).
* <br><br>
* The full set of rules is in the CardRules class.
*
*
* @author Forge
*/
public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet, IPaperCard, Serializable {
@@ -48,12 +49,19 @@ public final class PaperCard implements Comparable<IPaperCard>, 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<IPaperCard>, 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<IPaperCard>, 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<IPaperCard>, 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<IPaperCard>, 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<IPaperCard>, 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<IPaperCard>, 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<IPaperCard>, 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<IPaperCard>, 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;
}

View File

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

View File

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

View File

@@ -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<String> readAllLines(final File file, final boolean mayTrim) {
final List<String> 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 <name, url> pairs. if the name is not in the file, it is synthesized from the url
public static List<Pair<String, String>> readNameUrlFile(String nameUrlFile) {

View File

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

View File

@@ -15,147 +15,155 @@ import java.util.MissingResourceException;
import java.util.ResourceBundle;
public class Localizer {
private static Localizer instance;
private List<LocalizationChangeObserver> 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<LocalizationChangeObserver> 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<Language> 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<Language> 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;
}
}

View File

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

View File

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

View File

@@ -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<AbilityKey, Object> 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<AbilityKey, Object> 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<GameEntity, Map<Card, Integer>> 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<Card, Map<GameEntity, Integer>> et : damageMap.rowMap().entrySet()) {
final Card sourceLKI = et.getKey();
int sum = 0;
for (Map.Entry<GameEntity, Integer> 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();
}
}

View File

@@ -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<Card, Map<String, String>> preventionShieldsWithEffects = Maps.newTreeMap();
protected Map<CounterType, Integer> 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<AbilityKey, Object> 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<AbilityKey, Object> 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<AbilityKey, Object> 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<Card, Map<String, String>> getPreventNextDamageWithEffect() {
return preventionShieldsWithEffects;
}
public int getPreventNextDamageTotalShields() {
int shields = preventNextDamage;
for (final Map<String, String> 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<String, String> 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);

View File

@@ -20,6 +20,13 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
private Table<Optional<Player>, GameEntity, Map<CounterType, Integer>> dataMap = HashBasedTable.create();
public GameEntityCounterTable() {
}
public GameEntityCounterTable(Table<Optional<Player>, GameEntity, Map<CounterType, Integer>> counterTable) {
putAll(counterTable);
}
/*
* (non-Javadoc)
* @see com.google.common.collect.ForwardingTable#delegate()

View File

@@ -101,9 +101,10 @@ public class GameFormat implements Comparable<GameFormat> {
this.effectiveDate = effectiveDate;
if(sets != null) {
StaticData data = StaticData.instance();
Set<String> 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;
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -97,6 +97,7 @@ public enum ApiType {
LookAt (LookAtEffect.class),
LoseLife (LifeLoseEffect.class),
LosesGame (GameLossEffect.class),
MakeCard (MakeCardEffect.class),
Mana (ManaEffect.class),
ManaReflected (ManaReflectedEffect.class),
Manifest (ManifestEffect.class),

View File

@@ -21,9 +21,10 @@ import forge.game.GameObject;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardUtil;
import forge.game.card.CardZoneTable;
import forge.game.combat.Combat;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.replacement.ReplacementEffect;
@@ -55,7 +56,7 @@ public abstract class SpellAbilityEffect {
public abstract void resolve(SpellAbility sa);
protected String getStackDescription(final SpellAbility sa) {
// Unless overriden, let the spell description also be the stack description
// Unless overridden, let the spell description also be the stack description
return sa.getDescription();
}
@@ -123,7 +124,7 @@ public abstract class SpellAbilityEffect {
if (sa.hasParam("Announce")) {
String svar = sa.getParam("Announce");
int amount = CardFactoryUtil.xCount(sa.getHostCard(), sa.getSVar(svar));
int amount = AbilityUtils.calculateAmount(sa.getHostCard(), svar, sa);
sb.append(" ");
sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace(svar,"=",String.valueOf(amount))));
} else{
@@ -198,7 +199,7 @@ public abstract class SpellAbilityEffect {
// Players
protected final static PlayerCollection getTargetPlayers(final SpellAbility sa) { return getPlayers(false, "Defined", sa); }
protected final static PlayerCollection getTargetPlayers(final SpellAbility sa, final String definedParam) { return getPlayers(false, definedParam, sa); }
protected final static PlayerCollection getDefinedPlayersOrTargeted(final SpellAbility sa ) { return getPlayers(true, "Defined", sa); }
protected final static PlayerCollection getDefinedPlayersOrTargeted(final SpellAbility sa) { return getPlayers(true, "Defined", sa); }
protected final static PlayerCollection getDefinedPlayersOrTargeted(final SpellAbility sa, final String definedParam) { return getPlayers(true, definedParam, sa); }
private static PlayerCollection getPlayers(final boolean definedFirst, final String definedParam, final SpellAbility sa) {
@@ -218,7 +219,6 @@ public abstract class SpellAbilityEffect {
: AbilityUtils.getDefinedSpellAbilities(sa.getHostCard(), sa.getParam(definedParam), sa);
}
// Targets of card or player type
protected final static List<GameEntity> getTargetEntities(final SpellAbility sa) { return getEntities(false, "Defined", sa); }
protected final static List<GameEntity> getTargetEntities(final SpellAbility sa, final String definedParam) { return getEntities(false, definedParam, sa); }
@@ -297,7 +297,7 @@ public abstract class SpellAbilityEffect {
}
delTrig.append("| TriggerDescription$ ").append(desc);
final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), sa.getHostCard(), intrinsic);
final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), CardUtil.getLKICopy(sa.getHostCard()), intrinsic);
for (final Card c : crds) {
trig.addRemembered(c);
@@ -576,6 +576,8 @@ public abstract class SpellAbilityEffect {
GameEntity defender = null;
FCollection<GameEntity> defs = null;
// important to update defenders here, maybe some PW got removed
combat.initConstraints();
if ("True".equalsIgnoreCase(attacking)) {
defs = (FCollection<GameEntity>) combat.getDefenders();
} else if (sa.hasParam("ChoosePlayerOrPlaneswalker")) {
@@ -697,4 +699,52 @@ public abstract class SpellAbilityEffect {
}
}
}
protected static void addUntilCommand(final SpellAbility sa, GameCommand until) {
Card host = sa.getHostCard();
final Game game = host.getGame();
final String duration = sa.getParam("Duration");
// in case host was LKI
if (host.isLKI()) {
host = game.getCardState(host);
}
if ("UntilEndOfCombat".equals(duration)) {
game.getEndOfCombat().addUntil(until);
} else if ("UntilYourNextUpkeep".equals(duration)) {
game.getUpkeep().addUntil(sa.getActivatingPlayer(), until);
} else if ("UntilTheEndOfYourNextUpkeep".equals(duration)) {
if (game.getPhaseHandler().is(PhaseType.UPKEEP)) {
game.getUpkeep().registerUntilEnd(host.getController(), until);
} else {
game.getUpkeep().addUntilEnd(host.getController(), until);
}
} else if ("UntilTheEndOfYourNextTurn".equals(duration)) {
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
game.getEndOfTurn().registerUntilEnd(sa.getActivatingPlayer(), until);
} else {
game.getEndOfTurn().addUntilEnd(sa.getActivatingPlayer(), until);
}
} else if (duration != null && duration.startsWith("UntilAPlayerCastSpell")) {
game.getStack().addCastCommand(duration.split(" ")[1], until);
} else if ("UntilHostLeavesPlay".equals(duration)) {
host.addLeavesPlayCommand(until);
} else if ("UntilHostLeavesPlayOrEOT".equals(duration)) {
host.addLeavesPlayCommand(until);
game.getEndOfTurn().addUntil(until);
} else if ("UntilLoseControlOfHost".equals(duration)) {
host.addLeavesPlayCommand(until);
host.addChangeControllerCommand(until);
} else if ("UntilYourNextTurn".equals(duration)) {
game.getCleanup().addUntil(sa.getActivatingPlayer(), until);
} else if ("UntilUntaps".equals(duration)) {
host.addUntapCommand(until);
} else if ("UntilUnattached".equals(duration)) {
sa.getHostCard().addUnattachCommand(until);
} else if ("UntilFacedown".equals(duration)) {
sa.getHostCard().addFacedownCommand(until);
}else {
game.getEndOfTurn().addUntil(until);
}
}
}

View File

@@ -16,13 +16,11 @@ public class AddTurnEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
final int numTurns = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumTurns"), sa);
List<Player> tgtPlayers = getTargetPlayers(sa);
for (final Player player : tgtPlayers) {
sb.append(player).append(" ");
}

View File

@@ -43,7 +43,7 @@ public class AnimateAllEffect extends AnimateEffectBase {
// Every Animate event needs a unique time stamp
final long timestamp = game.getNextTimestamp();
final boolean permanent = sa.hasParam("Permanent");
final boolean permanent = "Permanent".equals(sa.getParam("Duration"));
final CardType types = new CardType(true);
if (sa.hasParam("Types")) {
@@ -158,13 +158,7 @@ public class AnimateAllEffect extends AnimateEffectBase {
};
if (!permanent) {
if (sa.hasParam("UntilEndOfCombat")) {
game.getEndOfCombat().addUntil(unanimate);
} else if (sa.hasParam("UntilYourNextTurn")) {
game.getCleanup().addUntil(host.getController(), unanimate);
} else {
game.getEndOfTurn().addUntil(unanimate);
}
addUntilCommand(sa, unanimate);
}
}
} // animateAllResolve

View File

@@ -29,8 +29,8 @@ public class AnimateEffect extends AnimateEffectBase {
String animateImprinted = null;
//if host is not on the battlefield don't apply
if ((sa.hasParam("UntilHostLeavesPlay") || sa.hasParam("UntilLoseControlOfHost"))
&& !sa.getHostCard().isInPlay()) {
if (("UntilHostLeavesPlay".equals(sa.getParam("Duration")) || "UntilLoseControlOfHost".equals(sa.getParam("Duration")))
&& !source.isInPlay()) {
return;
}
@@ -216,7 +216,7 @@ public class AnimateEffect extends AnimateEffectBase {
toughness = AbilityUtils.calculateAmount(host, sa.getParam("Toughness"), sa);
}
final boolean permanent = sa.hasParam("Permanent");
final boolean permanent = "Permanent".equals(sa.getParam("Duration"));
final List<String> types = Lists.newArrayList();
if (sa.hasParam("Types")) {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
@@ -296,15 +296,16 @@ public class AnimateEffect extends AnimateEffectBase {
// sb.append(abilities)
// sb.append(triggers)
if (!permanent) {
if (sa.hasParam("UntilEndOfCombat")) {
final String duration = sa.getParam("Duration");
if ("UntilEndOfCombat".equals(duration)) {
sb.append(" until end of combat.");
} else if (sa.hasParam("UntilHostLeavesPlay")) {
} else if ("UntilHostLeavesPlay".equals(duration)) {
sb.append(" until ").append(host).append(" leaves the battlefield.");
} else if (sa.hasParam("UntilYourNextUpkeep")) {
} else if ("UntilYourNextUpkeep".equals(duration)) {
sb.append(" until your next upkeep.");
} else if (sa.hasParam("UntilYourNextTurn")) {
} else if ("UntilYourNextTurn".equals(duration)) {
sb.append(" until your next turn.");
} else if (sa.hasParam("UntilControllerNextUntap")) {
} else if ("UntilControllerNextUntap".equals(duration)) {
sb.append(" until its controller's next untap step.");
} else {
sb.append(" until end of turn.");
@@ -313,7 +314,6 @@ public class AnimateEffect extends AnimateEffectBase {
sb.append(".");
}
return sb.toString();
}

View File

@@ -32,7 +32,6 @@ import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.event.GameEventCardStatsChanged;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
import forge.game.spellability.AbilityStatic;
@@ -111,6 +110,10 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
removeEnchantmentTypes = true;
}
if (sa.hasParam("RememberAnimated")) {
source.addRemembered(c);
}
if ((power != null) || (toughness != null)) {
c.addNewPT(power, toughness, timestamp);
}
@@ -214,30 +217,11 @@ public abstract class AnimateEffectBase extends SpellAbilityEffect {
addedStaticAbilities, removeAll, false, removeLandTypes, timestamp);
}
if (!sa.hasParam("Permanent")) {
if (sa.hasParam("UntilEndOfCombat")) {
game.getEndOfCombat().addUntil(unanimate);
} else if (sa.hasParam("UntilHostLeavesPlay")) {
source.addLeavesPlayCommand(unanimate);
} else if (sa.hasParam("UntilLoseControlOfHost")) {
sa.getHostCard().addLeavesPlayCommand(unanimate);
sa.getHostCard().addChangeControllerCommand(unanimate);
} else if (sa.hasParam("UntilYourNextUpkeep")) {
game.getUpkeep().addUntil(source.getController(), unanimate);
} else if (sa.hasParam("UntilTheEndOfYourNextUpkeep")) {
if (game.getPhaseHandler().is(PhaseType.UPKEEP)) {
game.getUpkeep().registerUntilEnd(source.getController(), unanimate);
} else {
game.getUpkeep().addUntilEnd(source.getController(), unanimate);
}
} else if (sa.hasParam("UntilControllerNextUntap")) {
if (!"Permanent".equals(sa.getParam("Duration"))) {
if ("UntilControllerNextUntap".equals(sa.getParam("Duration"))) {
game.getUntap().addUntil(c.getController(), unanimate);
} else if (sa.hasParam("UntilAPlayerCastSpell")) {
game.getStack().addCastCommand(sa.getParam("UntilAPlayerCastSpell"), unanimate);
} else if (sa.hasParam("UntilYourNextTurn")) {
game.getCleanup().addUntil(source.getController(), unanimate);
} else {
game.getEndOfTurn().addUntil(unanimate);
addUntilCommand(sa, unanimate);
}
}
}

View File

@@ -44,7 +44,7 @@ public class AssignGroupEffect extends SpellAbilityEffect {
Player chooser = sa.getActivatingPlayer();
if (sa.hasParam("Chooser")) {
final String choose = sa.getParam("Chooser");
chooser = AbilityUtils.getDefinedPlayers(sa.getHostCard(), choose, sa).get(0);
chooser = AbilityUtils.getDefinedPlayers(host, choose, sa).get(0);
}
Multimap<SpellAbility, GameObject> result = ArrayListMultimap.create();

View File

@@ -29,7 +29,7 @@ public class BlockEffect extends SpellAbilityEffect {
List<Card> attackers = new ArrayList<>();
if (sa.hasParam("DefinedAttacker")) {
for (final Card attacker : AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DefinedAttacker"), sa)) {
for (final Card attacker : AbilityUtils.getDefinedCards(host, sa.getParam("DefinedAttacker"), sa)) {
if (combat.isAttacking(attacker))
attackers.add(attacker);
}
@@ -37,7 +37,7 @@ public class BlockEffect extends SpellAbilityEffect {
List<Card> blockers = new ArrayList<>();
if (sa.hasParam("DefinedBlocker")) {
for (final Card blocker : AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DefinedBlocker"), sa)) {
for (final Card blocker : AbilityUtils.getDefinedCards(host, sa.getParam("DefinedBlocker"), sa)) {
if (blocker.isCreature() && blocker.isInZone(ZoneType.Battlefield))
blockers.add(blocker);
}

View File

@@ -31,9 +31,7 @@ public class CamouflageEffect extends SpellAbilityEffect {
}
}
if (attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.") &&
blockers.size() < defender.getCreaturesInPlay().size() ||
blockers.size() < CombatUtil.needsBlockers(attacker)) {
if (blockers.size() < CombatUtil.getMinNumBlockersForAttacker(attacker, defender)) {
// If not enough remaining creatures to block, don't add them as blocker
continue;
}

View File

@@ -78,21 +78,17 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
TargetChoices newTargetBlock = oldTargetBlock.clone();
// gets the divied value from old target
Integer div = oldTargetBlock.getDividedValue(oldTarget);
newTargetBlock.remove(oldTarget);
replaceIn.updateTarget(newTargetBlock, sa.getHostCard());
// 3. test if updated choices would be correct.
GameObject newTarget = Iterables.getFirst(getDefinedCardsOrTargeted(sa, "DefinedMagnet"), null);
if (replaceIn.getSpellAbility(true).canTarget(newTarget)) {
newTargetBlock.remove(oldTarget);
newTargetBlock.add(newTarget);
if (div != null) {
newTargetBlock.addDividedAllocation(newTarget, div);
}
replaceIn.updateTarget(newTargetBlock, sa.getHostCard());
}
else {
replaceIn.updateTarget(oldTargetBlock, sa.getHostCard());
}
}
else {
while(changingTgtSI != null) {
@@ -103,6 +99,14 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
int div = changingTgtSA.getTotalDividedValue();
changingTgtSA.resetTargets();
List<GameEntity> candidates = changingTgtSA.getTargetRestrictions().getAllCandidates(changingTgtSA, true);
if (sa.hasParam("RandomTargetRestriction")) {
candidates.removeIf(new java.util.function.Predicate<GameEntity>() {
@Override
public boolean test(GameEntity c) {
return !c.isValid(sa.getParam("RandomTargetRestriction").split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
}
});
}
GameEntity choice = Aggregates.random(candidates);
changingTgtSA.getTargets().add(choice);
if (changingTgtSA.isDividedAsYouChoose()) {

View File

@@ -26,7 +26,7 @@ public class ChangeTextEffect extends SpellAbilityEffect {
final Card source = sa.getHostCard();
final Game game = source.getGame();
final Long timestamp = Long.valueOf(game.getNextTimestamp());
final boolean permanent = sa.hasParam("Permanent");
final boolean permanent = "Permanent".equals(sa.getParam("Duration"));
final String changedColorWordOriginal, changedColorWordNew;
if (sa.hasParam("ChangeColorWord")) {
@@ -151,7 +151,7 @@ public class ChangeTextEffect extends SpellAbilityEffect {
changedTypeWordNew = null;
}
final boolean permanent = sa.hasParam("Permanent");
final boolean permanent = "Permanent".equals(sa.getParam("Duration"));
final StringBuilder sb = new StringBuilder();
sb.append("Change the text of ");

View File

@@ -42,8 +42,10 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard();
//if host is not on the battlefield don't apply
if (sa.hasParam("UntilHostLeavesPlay") && !sa.getHostCard().isInPlay()) {
if ("UntilHostLeavesPlay".equals(sa.getParam("Duration")) && !source.isInPlay()) {
return;
}
@@ -53,7 +55,6 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
CardCollection cards;
List<Player> tgtPlayers = getTargetPlayers(sa);
final Game game = sa.getActivatingPlayer().getGame();
final Card source = sa.getHostCard();
if ((!sa.usesTargeting() && !sa.hasParam("Defined")) || sa.hasParam("UseAllOriginZones")) {
cards = new CardCollection(game.getCardsIn(origin));
@@ -122,7 +123,7 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
}
if (sa.hasParam("ForgetOtherRemembered")) {
sa.getHostCard().clearRemembered();
source.clearRemembered();
}
final String remember = sa.getParam("RememberChanged");
@@ -203,9 +204,6 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
if (sa.hasParam("ExileFaceDown")) {
movedCard.turnFaceDown(true);
}
if (sa.hasParam("Tapped")) {
movedCard.setTapped(true);
}
}
if (remember != null) {
@@ -270,8 +268,8 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
triggerList.triggerChangesZoneAll(game, sa);
if (sa.hasParam("UntilHostLeavesPlay")) {
source.addLeavesPlayCommand(untilHostLeavesPlayCommand(triggerList, source));
if (sa.hasParam("Duration")) {
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, source));
}
// if Shuffle parameter exists, and any amount of cards were owned by

View File

@@ -57,17 +57,9 @@ import forge.util.collect.FCollectionView;
public class ChangeZoneEffect extends SpellAbilityEffect {
private boolean isHidden(SpellAbility sa) {
boolean hidden = sa.hasParam("Hidden");
if (!hidden && sa.hasParam("Origin")) {
hidden = ZoneType.isHidden(sa.getParam("Origin"));
}
return hidden;
}
@Override
protected String getStackDescription(SpellAbility sa) {
if (isHidden(sa)) {
if (sa.isHidden()) {
return changeHiddenOriginStackDescription(sa);
}
return changeKnownOriginStackDescription(sa);
@@ -97,13 +89,13 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
// Player whose cards will change zones
List<Player> fetchers = null;
if (sa.hasParam("DefinedPlayer")) {
fetchers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("DefinedPlayer"), sa);
fetchers = AbilityUtils.getDefinedPlayers(host, sa.getParam("DefinedPlayer"), sa);
}
if (fetchers == null && sa.hasParam("ValidTgts") && sa.usesTargeting()) {
fetchers = Lists.newArrayList(sa.getTargets().getTargetPlayers());
}
if (fetchers == null) {
fetchers = Lists.newArrayList(sa.getHostCard().getController());
fetchers = Lists.newArrayList(host.getController());
}
final String fetcherNames = Lang.joinHomogenous(fetchers, Player.Accessors.FN_GET_NAME);
@@ -111,7 +103,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
// Player who chooses the cards to move
List<Player> choosers = Lists.newArrayList();
if (sa.hasParam("Chooser")) {
choosers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Chooser"), sa);
choosers = AbilityUtils.getDefinedPlayers(host, sa.getParam("Chooser"), sa);
}
if (choosers.isEmpty()) {
choosers.add(sa.getActivatingPlayer());
@@ -420,11 +412,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
//if host is not on the battlefield don't apply
if (sa.hasParam("UntilHostLeavesPlay") && !sa.getHostCard().isInPlay()) {
if ("UntilHostLeavesPlay".equals(sa.getParam("Duration")) && !sa.getHostCard().isInPlay()) {
return;
}
if (isHidden(sa) && !sa.hasParam("Ninjutsu")) {
if (sa.isHidden() && !sa.hasParam("Ninjutsu")) {
changeHiddenOriginResolve(sa);
} else {
//else if (isKnown(origin) || sa.containsKey("Ninjutsu")) {
@@ -502,7 +494,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
Player chooser = player;
if (sa.hasParam("Chooser")) {
chooser = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Chooser"), sa).get(0);
chooser = AbilityUtils.getDefinedPlayers(hostCard, sa.getParam("Chooser"), sa).get(0);
}
for (final Card tgtC : tgtCards) {
@@ -510,7 +502,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
// this should check Self too
if (gameCard == null || !tgtC.equalsWithTimestamp(gameCard)) {
if (gameCard == null || !tgtC.equalsWithTimestamp(gameCard) || gameCard.isPhasedOut()) {
continue;
}
if (sa.usesTargeting() && !gameCard.canBeTargetedBy(sa)) {
@@ -636,8 +628,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
hostCard.removeRemembered(gameCard);
}
// Auras without Candidates stay in their current
// location
// Auras without Candidates stay in their current location
if (gameCard.isAura()) {
final SpellAbility saAura = gameCard.getFirstAttachSpell();
if (saAura != null) {
@@ -679,12 +670,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
game.getCombat().getBandOfAttacker(movedCard).setBlocked(false);
combatChanged = true;
}
if (sa.hasParam("Tapped") || sa.hasParam("Ninjutsu")) {
movedCard.setTapped(true);
}
if (sa.hasParam("Untapped")) {
movedCard.setTapped(false);
}
movedCard.setTimestamp(ts);
} else {
// might set before card is moved only for nontoken
@@ -812,12 +797,11 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
triggerList.triggerChangesZoneAll(game, sa);
counterTable.triggerCountersPutAll(game);
if (sa.hasParam("AtEOT") && !triggerList.isEmpty()) {
registerDelayedTrigger(sa, sa.getParam("AtEOT"), triggerList.allCards());
}
if (sa.hasParam("UntilHostLeavesPlay")) {
hostCard.addLeavesPlayCommand(untilHostLeavesPlayCommand(triggerList, hostCard));
if ("UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, hostCard));
}
// for things like Gaea's Blessing
@@ -1292,11 +1276,6 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
setFaceDownState(c, sa);
}
movedCard = game.getAction().moveToPlay(c, c.getController(), sa, moveParams);
if (sa.hasParam("Tapped")) {
movedCard.setTapped(true);
} else if (sa.hasParam("Untapped")) {
c.setTapped(false);
}
movedCard.setTimestamp(ts);
}
@@ -1402,8 +1381,8 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
triggerList.triggerChangesZoneAll(game, sa);
if (sa.hasParam("UntilHostLeavesPlay")) {
source.addLeavesPlayCommand(untilHostLeavesPlayCommand(triggerList, source));
if ("UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
addUntilCommand(sa, untilHostLeavesPlayCommand(triggerList, source));
}
}

View File

@@ -35,7 +35,7 @@ public class ChoosePlayerEffect extends SpellAbilityEffect {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final FCollectionView<Player> choices = sa.hasParam("Choices") ? AbilityUtils.getDefinedPlayers(
sa.getHostCard(), sa.getParam("Choices"), sa) : sa.getActivatingPlayer().getGame().getPlayersInTurnOrder();
card, sa.getParam("Choices"), sa) : sa.getActivatingPlayer().getGame().getPlayersInTurnOrder();
final String choiceDesc = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChoosePlayer");
final boolean random = sa.hasParam("Random");

View File

@@ -5,11 +5,11 @@ import java.util.List;
import org.apache.commons.lang3.StringUtils;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
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.player.Player;
import forge.game.spellability.SpellAbility;
@@ -84,7 +84,6 @@ public class ChooseSourceEffect extends SpellAbilityEffect {
if (sa.hasParam("Choices")) {
permanentSources = CardLists.getValidCards(permanentSources, sa.getParam("Choices"), host.getController(), host, sa);
stackSources = CardLists.getValidCards(stackSources, sa.getParam("Choices"), host.getController(), host, sa);
referencedSources = CardLists.getValidCards(referencedSources, sa.getParam("Choices"), host.getController(), host, sa);
commandZoneSources = CardLists.getValidCards(commandZoneSources, sa.getParam("Choices"), host.getController(), host, sa);
@@ -127,7 +126,7 @@ public class ChooseSourceEffect extends SpellAbilityEffect {
}
final String numericAmount = sa.getParamOrDefault("Amount", "1");
final int validAmount = StringUtils.isNumeric(numericAmount) ? Integer.parseInt(numericAmount) : CardFactoryUtil.xCount(host, host.getSVar(numericAmount));
final int validAmount = StringUtils.isNumeric(numericAmount) ? Integer.parseInt(numericAmount) : AbilityUtils.calculateAmount(host, numericAmount, sa);
for (final Player p : tgtPlayers) {
final CardCollection chosen = new CardCollection();
@@ -137,7 +136,7 @@ public class ChooseSourceEffect extends SpellAbilityEffect {
Card o = null;
do {
o = p.getController().chooseSingleEntityForEffect(sourcesToChooseFrom, sa, choiceTitle, null);
} while (o == null);
} while (o == null || o.getName().startsWith("--"));
chosen.add(o);
sourcesToChooseFrom.remove(o);
}

View File

@@ -51,10 +51,11 @@ public class ClashEffect extends SpellAbilityEffect {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Player, player);
runParams.put(AbilityKey.Won, player.equals(winner) ? "True" : "False");
sa.getHostCard().getGame().getTriggerHandler().runTrigger(TriggerType.Clashed, runParams, false);
runParams.put(AbilityKey.Player, opponent);
runParams.put(AbilityKey.Won, opponent.equals(winner) ? "True" : "False");
sa.getHostCard().getGame().getTriggerHandler().runTrigger(TriggerType.Clashed, runParams, false);
source.getGame().getTriggerHandler().runTrigger(TriggerType.Clashed, runParams, false);
final Map<AbilityKey, Object> runParams2 = AbilityKey.newMap();
runParams2.put(AbilityKey.Player, opponent);
runParams2.put(AbilityKey.Won, opponent.equals(winner) ? "True" : "False");
source.getGame().getTriggerHandler().runTrigger(TriggerType.Clashed, runParams2, false);
}
/**

View File

@@ -3,7 +3,6 @@ package forge.game.ability.effects;
import java.util.Arrays;
import java.util.List;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -143,6 +142,12 @@ public class CloneEffect extends SpellAbilityEffect {
tgtCard.updateStateForView();
// when clone is itself, cleanup from old abilities
if (host.equals(tgtCard)) {
tgtCard.clearImprintedCards();
tgtCard.clearRemembered();
}
// check if clone is now an Aura that needs to be attached
if (tgtCard.isAura() && !tgtCard.isInZone(ZoneType.Battlefield)) {
AttachEffect.attachAuraOnIndirectEnterBattlefield(tgtCard);
@@ -150,7 +155,7 @@ public class CloneEffect extends SpellAbilityEffect {
if (sa.hasParam("Duration")) {
final Card cloneCard = tgtCard;
// if clone is temporary, target needs old values back after
// if clone is temporary, target needs old values back after (keep Death-Mask Duplicant working)
final Iterable<Card> clonedImprinted = new CardCollection(tgtCard.getImprintedCards());
final Iterable<Object> clonedRemembered = new FCollection<>(tgtCard.getRemembered());
@@ -164,31 +169,19 @@ public class CloneEffect extends SpellAbilityEffect {
cloneCard.clearImprintedCards();
cloneCard.clearRemembered();
// restore original Remembered and Imprinted, ignore cards from players who lost
cloneCard.addImprintedCards(Iterables.filter(clonedImprinted, Predicates.not(CardPredicates.inZone(ZoneType.None))));
cloneCard.addImprintedCards(Iterables.filter(clonedImprinted, CardPredicates.ownerLives()));
cloneCard.addRemembered(Iterables.filter(clonedRemembered, Player.class));
cloneCard.addRemembered(Iterables.filter(Iterables.filter(clonedRemembered, Card.class), CardPredicates.ownerLives()));
cloneCard.addRemembered(Iterables.filter(Iterables.filter(clonedRemembered, Card.class), CardPredicates.ownerLives()));
cloneCard.updateStateForView();
game.fireEvent(new GameEventCardStatsChanged(cloneCard));
}
}
};
final String duration = sa.getParam("Duration");
if (duration.equals("UntilEndOfTurn")) {
game.getEndOfTurn().addUntil(unclone);
}
else if (duration.equals("UntilYourNextTurn")) {
game.getCleanup().addUntil(host.getController(), unclone);
}
else if (duration.equals("UntilUnattached")) {
sa.getHostCard().addUnattachCommand(unclone);
}
else if (duration.equals("UntilFacedown")) {
sa.getHostCard().addFacedownCommand(unclone);
}
addUntilCommand(sa, unclone);
}
//Clear Remembered and Imprint lists
// now we can also cleanup in case target was another card
tgtCard.clearRemembered();
tgtCard.clearImprintedCards();

View File

@@ -99,8 +99,8 @@ public class ControlExchangeEffect extends SpellAbilityEffect {
object2.setController(player1, tStamp);
object1.setController(player2, tStamp);
if (sa.hasParam("RememberExchanged")) {
sa.getHostCard().addRemembered(object1);
sa.getHostCard().addRemembered(object2);
host.addRemembered(object1);
host.addRemembered(object2);
}
}

View File

@@ -24,7 +24,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
final List<Player> newController = getTargetPlayers(sa, "NewController");
final List<Player> newController = getDefinedPlayersOrTargeted(sa, "NewController");
if (newController.isEmpty()) {
newController.add(sa.getActivatingPlayer());
}
@@ -108,8 +108,8 @@ public class ControlGainEffect extends SpellAbilityEffect {
continue;
}
if (!tgtC.equals(sa.getHostCard()) && !sa.getHostCard().getGainControlTargets().contains(tgtC)) {
sa.getHostCard().addGainControlTarget(tgtC);
if (!tgtC.equals(source) && !source.getGainControlTargets().contains(tgtC)) {
source.addGainControlTarget(tgtC);
}
long tStamp = game.getNextTimestamp();
@@ -139,24 +139,24 @@ public class ControlGainEffect extends SpellAbilityEffect {
game.fireEvent(new GameEventCardStatsChanged(tgtC));
}
if (remember && !sa.getHostCard().isRemembered(tgtC)) {
sa.getHostCard().addRemembered(tgtC);
if (remember && !source.isRemembered(tgtC)) {
source.addRemembered(tgtC);
}
if (forget && sa.getHostCard().isRemembered(tgtC)) {
sa.getHostCard().removeRemembered(tgtC);
if (forget && source.isRemembered(tgtC)) {
source.removeRemembered(tgtC);
}
if (lose != null) {
final GameCommand loseControl = getLoseControlCommand(tgtC, tStamp, bTapOnLose, source);
if (lose.contains("LeavesPlay") && sa.getHostCard() != tgtC) { // Only return control if host and target are different cards
sa.getHostCard().addLeavesPlayCommand(loseControl);
if (lose.contains("LeavesPlay") && source != tgtC) { // Only return control if host and target are different cards
source.addLeavesPlayCommand(loseControl);
}
if (lose.contains("Untap")) {
sa.getHostCard().addUntapCommand(loseControl);
source.addUntapCommand(loseControl);
}
if (lose.contains("LoseControl")) {
sa.getHostCard().addChangeControllerCommand(loseControl);
source.addChangeControllerCommand(loseControl);
}
if (lose.contains("EOT")) {
game.getEndOfTurn().addUntil(loseControl);
@@ -169,7 +169,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
if (lose.contains("StaticCommandCheck")) {
String leftVar = sa.getSVar(sa.getParam("StaticCommandCheckSVar"));
String rightVar = sa.getParam("StaticCommandSVarCompare");
sa.getHostCard().addStaticCommandList(new Object[]{leftVar, rightVar, tgtC, loseControl});
source.addStaticCommandList(new Object[]{leftVar, rightVar, tgtC, loseControl});
}
if (lose.contains("UntilTheEndOfYourNextTurn")) {
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {

View File

@@ -3,6 +3,7 @@ package forge.game.ability.effects;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
@@ -21,6 +22,7 @@ import forge.game.player.Player;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import forge.util.CardTranslation;
import forge.util.Localizer;
@@ -162,6 +164,19 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
if (sa.hasParam("MayChooseTarget")) {
copy.setMayChooseNewTargets(true);
}
if (sa.hasParam("RandomTarget")){
List<GameEntity> candidates = copy.getTargetRestrictions().getAllCandidates(chosenSA, true);
if (sa.hasParam("RandomTargetRestriction")) {
candidates.removeIf(new Predicate<GameEntity>() {
@Override
public boolean test(GameEntity c) {
return !c.isValid(sa.getParam("RandomTargetRestriction").split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
}
});
}
GameEntity choice = Aggregates.random(candidates);
resetFirstTargetOnCopy(copy, choice, chosenSA);
}
// extra case for Epic to remove the keyword and the last part of the SpellAbility
if (sa.hasParam("Epic")) {

View File

@@ -41,7 +41,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect {
final Card host = sa.getHostCard();
final Player activator = sa.getActivatingPlayer();
final String type = sa.getParam("CounterType");
final int counterAmount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa);
final int counterAmount = AbilityUtils.calculateAmount(host, sa.getParam("CounterNum"), sa);
final String valid = sa.getParam("ValidCards");
final ZoneType zone = sa.hasParam("ValidZone") ? ZoneType.smartValueOf(sa.getParam("ValidZone")) : ZoneType.Battlefield;
final boolean etbcounter = sa.hasParam("ETB");
@@ -52,7 +52,7 @@ public class CountersPutAllEffect extends SpellAbilityEffect {
}
CardCollectionView cards = game.getCardsIn(zone);
cards = CardLists.getValidCards(cards, valid, host.getController(), sa.getHostCard(), sa);
cards = CardLists.getValidCards(cards, valid, host.getController(), host, sa);
if (sa.usesTargeting()) {
final Player pl = sa.getTargets().getFirstTargetedPlayer();

View File

@@ -155,16 +155,16 @@ public class CountersPutEffect extends SpellAbilityEffect {
}
Player chooser = activator;
if (sa.hasParam("Chooser")) {
List<Player> choosers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Chooser"), sa);
List<Player> choosers = AbilityUtils.getDefinedPlayers(card, sa.getParam("Chooser"), sa);
if (choosers.isEmpty()) {
return;
}
chooser = choosers.get(0);
}
int n = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParamOrDefault("ChoiceAmount",
int n = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("ChoiceAmount",
"1"), sa);
int m = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParamOrDefault("MinChoiceAmount",
int m = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("MinChoiceAmount",
sa.getParamOrDefault("ChoiceAmount", "1")), sa);
// no choices allowed
@@ -405,10 +405,10 @@ public class CountersPutEffect extends SpellAbilityEffect {
Player placer = activator;
if (sa.hasParam("Placer")) {
final String pstr = sa.getParam("Placer");
placer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), pstr, sa).get(0);
placer = AbilityUtils.getDefinedPlayers(card, pstr, sa).get(0);
}
int counterAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amount, sa);
int counterAmount = AbilityUtils.calculateAmount(card, amount, sa);
GameEntityCounterTable table = new GameEntityCounterTable();
@@ -422,7 +422,7 @@ public class CountersPutEffect extends SpellAbilityEffect {
List<String> keywords = Arrays.asList(sa.getParam("SharedKeywords").split(" & "));
List<ZoneType> zones = ZoneType.listValueOf(sa.getParam("SharedKeywordsZone"));
String[] restrictions = sa.hasParam("SharedRestrictions") ? sa.getParam("SharedRestrictions").split(",") : new String[]{"Card"};
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, sa.getHostCard());
keywords = CardFactoryUtil.sharedKeywords(keywords, restrictions, zones, card);
for (String k : keywords) {
resolvePerType(sa, placer, CounterType.getType(k), counterAmount, table);
}

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