diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 9f4428ae97c..d7a63cf928e 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -50,7 +50,6 @@ import forge.game.zone.ZoneType; import forge.util.Aggregates; import forge.util.Expressions; import forge.util.MyRandom; -import forge.util.TextUtil; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; @@ -333,25 +332,6 @@ public class AiAttackController { return attackers; } - // no need to block if an effect is in play which untaps all creatures (pseudo-Vigilance akin to - // Awakening or Prophet of Kruphix) - for (Card card : ai.getGame().getCardsIn(ZoneType.Battlefield)) { - boolean untapsEachTurn = card.hasSVar("UntapsEachTurn"); - boolean untapsEachOtherTurn = card.hasSVar("UntapsEachOtherPlayerTurn"); - - if (untapsEachTurn || untapsEachOtherTurn) { - String affected = untapsEachTurn ? card.getSVar("UntapsEachTurn") - : card.getSVar("UntapsEachOtherPlayerTurn"); - - for (String aff : TextUtil.split(affected, ',')) { - if (aff.equals("Creature") - && (untapsEachTurn || (untapsEachOtherTurn && ai.equals(card.getController())))) { - return attackers; - } - } - } - } - List opponentsAttackers = new ArrayList<>(oppList); opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate() { @Override @@ -370,7 +350,9 @@ public class AiAttackController { } continue; } - if (c.hasKeyword(Keyword.VIGILANCE)) { + // no need to block if an effect is in play which untaps all creatures + // (pseudo-Vigilance akin to Awakening or or Prophet of Kruphix) + if (c.hasKeyword(Keyword.VIGILANCE) || ComputerUtilCard.willUntap(ai, c)) { vigilantes.add(c); notNeededAsBlockers.remove(c); // they will be re-added later if (canBlockAnAttacker(c, opponentsAttackers, false)) { @@ -392,7 +374,7 @@ public class AiAttackController { } } int blockersStillNeeded = blockersNeeded - fixedBlockers; - blockersStillNeeded = Math.min(blockersNeeded, list.size()); + blockersStillNeeded = Math.min(blockersStillNeeded, list.size()); for (int i = 0; i < blockersStillNeeded; i++) { notNeededAsBlockers.remove(list.get(i)); } @@ -850,7 +832,7 @@ public class AiAttackController { return aiAggression; } - if (simAI && ai.isCardInPlay("Reconnaissance")) { + if (simAI && ComputerUtilCard.isNonDisabledCardInPlay(ai, "Reconnaissance")) { for (Card attacker : attackersLeft) { if (canAttackWrapper(attacker, defender)) { // simulation will decide if attacker stays in combat based on blocks diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 04e9397af74..736fb930112 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -927,30 +927,8 @@ public class AiController { return AiPlayDecision.WillPlay; } - public boolean isNonDisabledCardInPlay(final String cardName) { - for (Card card : player.getCardsIn(ZoneType.Battlefield)) { - if (card.getName().equals(cardName)) { - // TODO - Better logic to determine if a permanent is disabled by local effects - // currently assuming any permanent enchanted by another player - // is disabled and a second copy is necessary - // will need actual logic that determines if the enchantment is able - // to disable the permanent or it's still functional and a duplicate is unneeded. - boolean disabledByEnemy = false; - for (Card card2 : card.getEnchantedBy()) { - if (card2.getOwner() != player) { - disabledByEnemy = true; - } - } - if (!disabledByEnemy) { - return true; - } - } - } - return false; - } - private AiPlayDecision canPlaySpellBasic(final Card card, final SpellAbility sa) { - if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) { + if ("True".equals(card.getSVar("NonStackingEffect")) && ComputerUtilCard.isNonDisabledCardInPlay(player, card.getName())) { return AiPlayDecision.NeedsToPlayCriteriaNotMet; } @@ -1170,12 +1148,11 @@ public class AiController { public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa) { return getCardsToDiscard(numDiscard, uTypes, sa, CardCollection.EMPTY); } - public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa, final CardCollectionView exclude) { - boolean noFiltering = (sa != null) && "DiscardCMCX".equals(sa.getParam("AILogic")); // list AI logic for which filtering is taken care of elsewhere + boolean noFiltering = sa != null && "DiscardCMCX".equals(sa.getParam("AILogic")); // list AI logic for which filtering is taken care of elsewhere CardCollection hand = new CardCollection(player.getCardsIn(ZoneType.Hand)); hand.removeAll(exclude); - if ((uTypes != null) && (sa != null) && !noFiltering) { + if (uTypes != null && sa != null && !noFiltering) { hand = CardLists.getValidCards(hand, uTypes, sa.getActivatingPlayer(), sa.getHostCard(), sa); } return getCardsToDiscard(numDiscard, numDiscard, hand, sa); @@ -1214,7 +1191,7 @@ public class AiController { if ("DiscardUncastableAndExcess".equals(sa.getParam("AILogic"))) { CardCollection discards = new CardCollection(); final CardCollectionView inHand = player.getCardsIn(ZoneType.Hand); - final int numLandsOTB = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size(); + final int numLandsOTB = CardLists.count(player.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS); int numOppInHand = 0; for (Player p : player.getGame().getPlayers()) { if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) { @@ -1274,7 +1251,7 @@ public class AiController { if (validCards.isEmpty()) { continue; } - final int numLandsInPlay = CardLists.count(player.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS); + final int numLandsInPlay = CardLists.count(player.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA); final CardCollection landsInHand = CardLists.filter(validCards, CardPredicates.Presets.LANDS); final int numLandsInHand = landsInHand.size(); @@ -2361,7 +2338,7 @@ public class AiController { return Iterables.getFirst(doubleLife, null); } } else if (mode.equals(ReplacementType.DamageDone)) { - List prevention = filterList(list, CardTraitPredicates.hasParam("Prevention")); + List prevention = filterList(list, CardTraitPredicates.hasParam("Prevent")); // TODO when Protection is done as ReplacementEffect do them // before normal prevention diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index f130e08e967..dcb0b9acbd1 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -487,11 +487,11 @@ public class ComputerUtil { // Discard lands final CardCollection landsInHand = CardLists.getType(typeList, "Land"); if (!landsInHand.isEmpty()) { - final CardCollection landsInPlay = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Land"); + final int numLandsInPlay = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA); final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land"); final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, CardPredicates.Accessors.fnGetCmc)); - if (landsInPlay.size() >= highestCMC - || (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)) { + if (numLandsInPlay >= highestCMC + || (numLandsInPlay + landsInHand.size() > 6 && landsInHand.size() > 1)) { // Don't need more land. return ComputerUtilCard.getWorstLand(landsInHand); } @@ -1190,7 +1190,7 @@ public class ComputerUtil { } final Game game = ai.getGame(); - final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS); + final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA); final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS); final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land"); final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, CardPredicates.Accessors.fnGetCmc)); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 3d74f20b4fd..0bc2916d4a1 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -63,6 +63,7 @@ import forge.item.PaperCard; import forge.util.Aggregates; import forge.util.Expressions; import forge.util.MyRandom; +import forge.util.TextUtil; public class ComputerUtilCard { public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) { @@ -1816,6 +1817,9 @@ public class ComputerUtilCard { } public static boolean hasActiveUndyingOrPersist(final Card c) { + if (c.isToken()) { + return false; + } if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterEnumType.P1P1) == 0) { return true; } @@ -1982,6 +1986,50 @@ public class ComputerUtilCard { return totalCost; } + public static boolean willUntap(Player ai, Card tapped) { + // TODO use AiLogic on trigger in case card loses all abilities + // if it's from a static need to also check canUntap + for (Card card : ai.getGame().getCardsIn(ZoneType.Battlefield)) { + boolean untapsEachTurn = card.hasSVar("UntapsEachTurn"); + boolean untapsEachOtherTurn = card.hasSVar("UntapsEachOtherPlayerTurn"); + + if (untapsEachTurn || untapsEachOtherTurn) { + String affected = untapsEachTurn ? card.getSVar("UntapsEachTurn") + : card.getSVar("UntapsEachOtherPlayerTurn"); + + for (String aff : TextUtil.split(affected, ',')) { + if (tapped.isValid(aff, ai, tapped, null) + && (untapsEachTurn || (untapsEachOtherTurn && ai.equals(card.getController())))) { + return true; + } + } + } + } + return false; + } + + public static boolean isNonDisabledCardInPlay(final Player ai, final String cardName) { + for (Card card : ai.getCardsIn(ZoneType.Battlefield)) { + if (card.getName().equals(cardName)) { + // TODO - Better logic to determine if a permanent is disabled by local effects + // currently assuming any permanent enchanted by another player + // is disabled and a second copy is necessary + // will need actual logic that determines if the enchantment is able + // to disable the permanent or it's still functional and a duplicate is unneeded. + boolean disabledByEnemy = false; + for (Card card2 : card.getEnchantedBy()) { + if (card2.getOwner() != ai) { + disabledByEnemy = true; + } + } + if (!disabledByEnemy) { + return true; + } + } + } + return false; + } + // Determine if the AI has an AI:RemoveDeck:All or an AI:RemoveDeck:Random hint specified. // Includes a NPE guard on getRules() which might otherwise be tripped on some cards (e.g. tokens). public static boolean isCardRemAIDeck(final Card card) { diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java index ebbebbc3042..12536f21c3f 100644 --- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java @@ -11,13 +11,6 @@ import forge.game.keyword.Keyword; import forge.game.spellability.SpellAbility; public class CreatureEvaluator implements Function { - protected int getEffectivePower(final Card c) { - return c.getNetCombatDamage(); - } - protected int getEffectiveToughness(final Card c) { - return c.getNetToughness(); - } - @Override public Integer apply(Card c) { return evaluateCreature(c); @@ -31,8 +24,8 @@ public class CreatureEvaluator implements Function { if (!c.isToken()) { value += addValue(20, "non-token"); // tokens should be worth less than actual cards } - int power = getEffectivePower(c); - final int toughness = getEffectiveToughness(c); + int power = c.getNetCombatDamage(); + final int toughness = c.getNetToughness(); // TODO replace with ReplacementEffect checks if (c.hasKeyword("Prevent all combat damage that would be dealt by CARDNAME.") @@ -45,6 +38,11 @@ public class CreatureEvaluator implements Function { if (considerPT) { value += addValue(power * 15, "power"); value += addValue(toughness * 10, "toughness: " + toughness); + + // because backside is always stronger the potential makes it better than a single faced card + if (c.hasKeyword(Keyword.DAYBOUND)) { + value += addValue(power * 10, "transforming"); + } } if (considerCMC) { value += addValue(c.getCMC() * 5, "cmc"); @@ -72,8 +70,8 @@ public class CreatureEvaluator implements Function { if (c.hasKeyword(Keyword.MENACE)) { value += addValue(power * 4, "menace"); } - if (c.hasStartOfKeyword("CantBeBlockedBy")) { - value += addValue(power * 3, "block-restrict"); + if (c.hasKeyword(Keyword.SKULK)) { + value += addValue(power * 3, "skulk"); } } @@ -106,19 +104,18 @@ public class CreatureEvaluator implements Function { value += addValue(c.getKeywordMagnitude(Keyword.AFFLICT) * 5, "afflict"); } - value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido"); - value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking"); - value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted"); value += addValue(c.getKeywordMagnitude(Keyword.ANNIHILATOR) * 50, "eldrazi"); value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb"); // Keywords that may produce temporary or permanent buffs over time - if (c.hasKeyword(Keyword.PROWESS)) { - value += addValue(5, "prowess"); - } if (c.hasKeyword(Keyword.OUTLAST)) { value += addValue(10, "outlast"); } + value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido"); + value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking"); + value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted"); + value += addValue(c.getAmountOfKeyword(Keyword.MELEE) * 18, "melee"); + value += addValue(c.getAmountOfKeyword(Keyword.PROWESS) * 5, "prowess"); // Defensive Keywords if (c.hasKeyword(Keyword.REACH) && !c.hasKeyword(Keyword.FLYING)) { @@ -141,18 +138,41 @@ public class CreatureEvaluator implements Function { value += addValue(35, "hexproof"); } else if (c.hasKeyword(Keyword.SHROUD)) { value += addValue(30, "shroud"); + } else if (c.hasKeyword(Keyword.WARD)) { + value += addValue(10, "ward"); } if (c.hasKeyword(Keyword.PROTECTION)) { value += addValue(20, "protection"); } + for (final SpellAbility sa : c.getSpellAbilities()) { + if (sa.isAbility()) { + value += addValue(evaluateSpellAbility(sa), "sa: " + sa); + } + } + + // paired creatures are more valuable because they grant a bonus to the other creature + if (c.isPaired()) { + value += addValue(14, "paired"); + } + + if (c.hasEncodedCard()) { + value += addValue(24, "encoded"); + } + + if (ComputerUtilCard.hasActiveUndyingOrPersist(c)) { + value += addValue(30, "revive"); + } + // Bad keywords if (c.hasKeyword(Keyword.DEFENDER) || c.hasKeyword("CARDNAME can't attack.")) { value -= subValue((power * 9) + 40, "defender"); } else if (c.getSVar("SacrificeEndCombat").equals("True")) { value -= subValue(40, "sac-end"); } - if (c.hasKeyword("CARDNAME can't block.")) { + if (c.hasKeyword("CARDNAME can't attack or block.")) { + value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless + } else if (c.hasKeyword("CARDNAME can't block.")) { value -= subValue(10, "cant-block"); } else if (c.hasKeyword("CARDNAME attacks each combat if able.")) { value -= subValue(10, "must-attack"); @@ -165,10 +185,18 @@ public class CreatureEvaluator implements Function { if (c.hasSVar("DestroyWhenDamaged")) { value -= subValue((toughness - 1) * 9, "dies-to-dmg"); } - - if (c.hasKeyword("CARDNAME can't attack or block.")) { - value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless + if (c.getSVar("Targeting").equals("Dies")) { + value -= subValue(25, "dies"); } + + if (c.isUntapped()) { + value += addValue(1, "untapped"); + } + + if (!c.getManaAbilities().isEmpty()) { + value += addValue(10, "manadork"); + } + if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) { if (c.isTapped()) { value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless @@ -185,41 +213,20 @@ public class CreatureEvaluator implements Function { } else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) { value -= subValue(10, "echo-unpaid"); } - - if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) { - value -= subValue(20, "upkeep-dmg"); - } if (c.hasKeyword(Keyword.FADING)) { value -= subValue(20, "fading"); } if (c.hasKeyword(Keyword.VANISHING)) { value -= subValue(20, "vanishing"); } - if (c.getSVar("Targeting").equals("Dies")) { - value -= subValue(25, "dies"); + if (c.hasKeyword(Keyword.PHASING)) { + value -= subValue(10, "phasing"); } - for (final SpellAbility sa : c.getSpellAbilities()) { - if (sa.isAbility()) { - value += addValue(evaluateSpellAbility(sa), "sa: " + sa); - } - } - if (!c.getManaAbilities().isEmpty()) { - value += addValue(10, "manadork"); - } - - if (c.isUntapped()) { - value += addValue(1, "untapped"); - } - - // paired creatures are more valuable because they grant a bonus to the other creature - if (c.isPaired()) { - value += addValue(14, "paired"); - } - - if (!c.hasEncodedCard()) { - value += addValue(24, "encoded"); - } + // TODO no longer a KW + if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) { + value -= subValue(20, "upkeep-dmg"); + } // card-specific evaluation modifier if (c.hasSVar("AIEvaluationModifier")) { @@ -240,7 +247,7 @@ public class CreatureEvaluator implements Function { && (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) { if (sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) { // Electrostatic Pummeler, can be expanded for similar cards - int initPower = getEffectivePower(sa.getHostCard()); + int initPower = sa.getHostCard().getNetPower(); int pumpedPower = initPower; int energy = sa.getHostCard().getController().getCounters(CounterEnumType.ENERGY); if (energy > 0) { diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index 8b2d08ff9c7..3be4c90a85e 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -462,7 +462,7 @@ public class PlayerControllerAi extends PlayerController { } } - int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size(); + int landsOTB = CardLists.count(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA); if (!p.isOpponentOf(player)) { if (landsOTB <= 2) { @@ -611,7 +611,7 @@ public class PlayerControllerAi extends PlayerController { hand.removeAll(toReturn); CardCollection landsInHand = CardLists.filter(hand, Presets.LANDS); - int numLandsInHand = landsInHand.size() - CardLists.filter(toReturn, Presets.LANDS).size(); + int numLandsInHand = landsInHand.size() - CardLists.count(toReturn, Presets.LANDS); // If we're flooding with lands, get rid of the worst land we have if (numLandsInHand > 0 && numLandsInHand > numLandsDesired) { diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index 0aadf73c21d..d6dc1101744 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -1558,8 +1558,7 @@ public class AttachAi extends SpellAbilityAi { final boolean evasive = keyword.equals("Unblockable") || keyword.equals("Fear") || keyword.equals("Intimidate") || keyword.equals("Shadow") || keyword.equals("Flying") || keyword.equals("Horsemanship") - || keyword.endsWith("walk") || keyword.startsWith("CantBeBlockedBy") - || keyword.equals("All creatures able to block CARDNAME do so."); + || keyword.endsWith("walk") || keyword.equals("All creatures able to block CARDNAME do so."); // give evasive keywords to creatures that can attack and deal damage boolean canBeBlocked = false; diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java index fd937ad8636..f88938f70a1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java @@ -122,10 +122,7 @@ public class ControlGainAi extends SpellAbilityAi { return true; } - CardCollection list = new CardCollection(); - for (Player pl : opponents) { - list.addAll(pl.getCardsIn(ZoneType.Battlefield)); - } + CardCollection list = opponents.getCardsIn(ZoneType.Battlefield); list = CardLists.getValidCards(list, tgt.getValidTgts(), sa.getActivatingPlayer(), sa.getHostCard(), sa); @@ -325,7 +322,7 @@ public class ControlGainAi extends SpellAbilityAi { } else { return this.canPlayAI(ai, sa); } - } // pumpDrawbackAI() + } @Override protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options, Map params) { diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java index 070a4025cdb..7cdc3b685f7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java @@ -124,7 +124,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 (!SpellAbilityAi.playReusable(ai,sa)) { + if (!SpellAbilityAi.playReusable(ai, sa)) { return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index c661b945473..7f7e1c991e6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -356,7 +356,8 @@ public class DamageDealAi extends DamageAiBase { return c.getSVar("Targeting").equals("Dies") || (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d) && !ComputerUtil.canRegenerate(ai, c) - && !(c.getSVar("SacMe").length() > 0); + && !(c.getSVar("SacMe").length() > 0) + && !ComputerUtilCard.hasActiveUndyingOrPersist(c); } }); @@ -489,10 +490,7 @@ public class DamageDealAi extends DamageAiBase { */ private boolean damageTargetAI(final Player ai, final SpellAbility saMe, final int dmg, final boolean immediately) { final TargetRestrictions tgt = saMe.getTargetRestrictions(); - if ("Atarka's Command".equals(ComputerUtilAbility.getAbilitySourceName(saMe))) { - // playReusable in damageChooseNontargeted wrongly assumes that CharmEffect options are re-usable - return shouldTgtP(ai, saMe, dmg, false); - } + if (tgt == null) { return damageChooseNontargeted(ai, saMe, dmg); } @@ -834,6 +832,10 @@ public class DamageDealAi extends DamageAiBase { } } } + if ("Atarka's Command".equals(ComputerUtilAbility.getAbilitySourceName(saMe))) { + // playReusable wrongly assumes that CharmEffect options are re-usable + return positive; + } if (!positive && !(saMe instanceof AbilitySub)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java index 699cdc4df17..5f2e7954c57 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -208,7 +208,7 @@ public class DestroyAi extends SpellAbilityAi { list = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return (c.getShieldCount() == 0 && !ComputerUtil.canRegenerate(ai, c)); + return c.getShieldCount() == 0 && !ComputerUtil.canRegenerate(ai, c); } }); } @@ -440,8 +440,8 @@ public class DestroyAi extends SpellAbilityAi { boolean nonBasicTgt = !tgtLand.isBasicLand(); // Try not to lose tempo too much and not to mana-screw yourself when considering this logic - int numLandsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA).size(); - int numLandsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size(); + int numLandsInHand = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA); + int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA); // If the opponent skipped a land drop, consider not looking at having the extra land in hand if the profile allows it boolean isHighPriority = highPriorityIfNoLandDrop && oppSkippedLandDrop; diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java index 27f228f3dd5..010260cfedd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java @@ -93,7 +93,7 @@ public class DiscardAi extends SpellAbilityAi { if (sa.hasParam("AnyNumber")) { if ("DiscardUncastableAndExcess".equals(aiLogic)) { final CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand); - final int numLandsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size(); + final int numLandsOTB = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS); int numDiscard = 0; int numOppInHand = 0; for (Player p : ai.getGame().getPlayers()) { @@ -143,7 +143,7 @@ public class DiscardAi extends SpellAbilityAi { // some other variables here, like handsize vs. maxHandSize return randomReturn; - } // discardCanPlayAI() + } private boolean discardTargetAI(final Player ai, final SpellAbility sa) { final PlayerCollection opps = ai.getOpponents(); @@ -164,7 +164,7 @@ public class DiscardAi extends SpellAbilityAi { } } return false; - } // discardTargetAI() + } @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { @@ -198,7 +198,7 @@ public class DiscardAi extends SpellAbilityAi { } return true; - } // discardTrigger() + } @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { @@ -209,10 +209,10 @@ public class DiscardAi extends SpellAbilityAi { } // TODO: check for some extra things return true; - } // discardCheckDrawbackAI() + } public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { - if ( mode == PlayerActionConfirmMode.Random ) { + if (mode == PlayerActionConfirmMode.Random) { // TODO For now AI will always discard Random used currently with: Balduvian Horde and similar cards return true; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java index 305cece5cdd..69813b33293 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java @@ -8,6 +8,7 @@ import com.google.common.base.Predicates; import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; +import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilMana; import forge.ai.PlayerControllerAi; @@ -16,7 +17,6 @@ import forge.card.ColorSet; import forge.card.MagicColor; import forge.card.mana.ManaAtom; import forge.card.mana.ManaCost; -import forge.game.GlobalRuleChange; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -65,7 +65,7 @@ public class ManaEffectAi extends SpellAbilityAi { */ @Override protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) { - if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && canRampPool(ai, sa.getHostCard())) { + if (ph.is(PhaseType.END_OF_TURN) && (ph.getNextTurn() == ai || ComputerUtilCard.willUntap(ai, sa.getHostCard())) && canRampPool(ai, sa.getHostCard())) { return true; } if (!ph.is(PhaseType.MAIN2) || !ComputerUtil.activateForCost(sa, ai)) { @@ -87,7 +87,7 @@ public class ManaEffectAi extends SpellAbilityAi { if (logic.startsWith("ManaRitual")) { return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai); } else if ("AtOppEOT".equals(logic)) { - return !ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.manaBurn) && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai; + return !ai.getManaPool().hasBurn() && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai; } return super.checkPhaseRestrictions(ai, sa, ph, logic); } @@ -106,7 +106,7 @@ public class ManaEffectAi extends SpellAbilityAi { PhaseHandler ph = ai.getGame().getPhaseHandler(); boolean moreManaNextTurn = false; - if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && canRampPool(ai, sa.getHostCard())) { + if (ph.is(PhaseType.END_OF_TURN) && (ph.getNextTurn() == ai || ComputerUtilCard.willUntap(ai, sa.getHostCard())) && canRampPool(ai, sa.getHostCard())) { moreManaNextTurn = true; } @@ -259,7 +259,7 @@ public class ManaEffectAi extends SpellAbilityAi { return castableSpells.size() > 0; } - private boolean canRampPool(Player ai, Card source) { + public static boolean canRampPool(Player ai, Card source) { ManaPool mp = ai.getManaPool(); Mana test = null; if (mp.isEmpty()) { diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java index ce7fc140e1c..9dd85db7268 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java @@ -244,7 +244,7 @@ public class PermanentAi extends SpellAbilityAi { // Only cast if there are X or more mana sources controlled by the AI *or* // if there are X-1 mana sources in play but the AI has an extra land in hand CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true); - int extraMana = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size() > 0 ? 1 : 0; + int extraMana = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS) > 0 ? 1 : 0; if (card.getName().equals("Illusions of Grandeur")) { // TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic. extraMana += Math.min(3, CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.or(CardPredicates.nameEquals("Sapphire Medallion"), CardPredicates.nameEquals("Helm of Awakening"))).size()) * 2; // each cost-reduction spell accounts for {1} in both Illusions and Donate diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java index e18e73c0550..e43a9f214f9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java @@ -139,7 +139,7 @@ public class PermanentCreatureAi extends PermanentAi { boolean hasETBTrigger = card.hasETBTrigger(true); boolean hasAmbushAI = card.hasSVar("AmbushAI"); boolean defOnlyAmbushAI = hasAmbushAI && "BlockOnly".equals(card.getSVar("AmbushAI")); - boolean hasFloatMana = ai.getManaPool().totalMana() > 0; + boolean loseFloatMana = ai.getManaPool().totalMana() > 0 && !ManaEffectAi.canRampPool(ai, card); boolean willDiscardNow = isOwnEOT && !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize(); boolean willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat); boolean wantToCastInMain1 = ph.is(PhaseType.MAIN1, ai) && ComputerUtil.castPermanentInMain1(ai, sa); @@ -176,7 +176,7 @@ public class PermanentCreatureAi extends PermanentAi { } } - if (hasFloatMana || willDiscardNow || willDieNow) { + if (loseFloatMana || willDiscardNow || willDieNow) { // Will lose mana in pool or about to discard a card in cleanup or about to die in combat, so use this opportunity return true; } else if (isCommander && isMyMain1OrLater) { diff --git a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java index 04f3e23379a..fdb61f45d76 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java @@ -37,7 +37,7 @@ public class PowerExchangeAi extends SpellAbilityAi { return c.canBeTargetedBy(sa) && c.getController() != ai; } }); - CardLists.sortByPowerAsc(list); + CardLists.sortByPowerDesc(list); c1 = list.isEmpty() ? null : list.get(0); if (sa.hasParam("Defined")) { c2 = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa).get(0); @@ -52,7 +52,7 @@ public class PowerExchangeAi extends SpellAbilityAi { if (c1 == null || c2 == null) { return false; } - if (ComputerUtilCard.evaluateCreature(c1) > ComputerUtilCard.evaluateCreature(c2) + 40) { + if (sa.isMandatory() || ComputerUtilCard.evaluateCreature(c1) > ComputerUtilCard.evaluateCreature(c2) + 40) { sa.getTargets().add(c1); return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); } diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java index 459ea2aa6e2..97e89c71426 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java @@ -200,7 +200,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { return false; } - final boolean evasive = (keyword.endsWith("Unblockable") || keyword.endsWith("Shadow") || keyword.startsWith("CantBeBlockedBy")); + final boolean evasive = keyword.endsWith("Unblockable") || keyword.endsWith("Shadow"); // give evasive keywords to creatures that can or do attack if (evasive) { return !ph.isPlayerTurn(opp) && (CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card))) diff --git a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java index b446d81c98c..8ce0f6b73da 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java @@ -109,7 +109,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { Player p = pc.getFirst(); // currently always a single target spell Card top = p.getCardsIn(ZoneType.Library).getFirst(); - int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size(); + int landsOTB = CardLists.count(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA); int cmc = top.isSplitCard() ? Math.min(top.getCMC(Card.SplitCMCMode.LeftSplitCMC), top.getCMC(Card.SplitCMCMode.RightSplitCMC)) : top.getCMC(); int maxCastable = ComputerUtilMana.getAvailableManaEstimate(p, false); diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java b/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java index 13da2ac6a57..be4fd91a180 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java @@ -24,6 +24,10 @@ public abstract class RevealAiBase extends SpellAbilityAi { opps = Lists.newArrayList(Iterables.filter(opps, PlayerPredicates.isTargetableBy(sa))); if (opps.isEmpty()) { + if (mandatory && sa.canTarget(ai)) { + sa.getTargets().add(ai); + return true; + } return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java index 96f1fe9a018..69fc263cb3a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java @@ -198,12 +198,13 @@ public abstract class TapAiBase extends SpellAbilityAi { Predicate findBlockers = CardPredicates.possibleBlockerForAtLeastOne(attackers); List creatureList = CardLists.filter(tapList, findBlockers); + // TODO check if own creature would be forced to attack and we want to keep it alive + if (!attackers.isEmpty() && !creatureList.isEmpty()) { choice = ComputerUtilCard.getBestCreatureAI(creatureList); } else if (sa.getRootAbility().isTrigger() || ComputerUtil.castSpellInMain1(ai, sa)) { choice = ComputerUtilCard.getMostExpensivePermanentAI(tapList); } - } else if (phase.isPlayerTurn(opp) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { // Tap creatures possible blockers before combat during AI's turn. diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java index 881b922ab68..0984685aed4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -26,6 +26,7 @@ import forge.game.cost.CostTap; import forge.game.mana.ManaCostBeingPaid; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; +import forge.game.phase.Untap; import forge.game.player.Player; import forge.game.player.PlayerCollection; import forge.game.spellability.SpellAbility; @@ -213,6 +214,7 @@ public class UntapAi extends SpellAbilityAi { untapList.remove(choice); list.remove(choice); + // TODO ComputerUtilCard.willUntap(ai, choice) sa.getTargets().add(choice); } return true; @@ -321,7 +323,7 @@ public class UntapAi extends SpellAbilityAi { } // See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself - CardCollection noAutoUntap = CardLists.filter(untapList, CardPredicates.hasKeyword("CARDNAME doesn't untap during your untap step.")); + CardCollection noAutoUntap = CardLists.filter(untapList, Predicates.not(Untap.CANUNTAP)); if (!noAutoUntap.isEmpty()) { return ComputerUtilCard.getBestAI(noAutoUntap); } diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java index 3618d1e4d72..1c351e04785 100644 --- a/forge-core/src/main/java/forge/StaticData.java +++ b/forge-core/src/main/java/forge/StaticData.java @@ -86,10 +86,12 @@ public class StaticData { final Map variantsCards = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); final Map customizedCards = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - for (CardEdition e : editions) { - if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) { - for (CardEdition.CardInSet cis : e.getAllCardsInSet()) { - funnyCards.add(cis.name); + if (!loadNonLegalCards) { + for (CardEdition e : editions) { + if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) { + for (CardEdition.CardInSet cis : e.getAllCardsInSet()) { + funnyCards.add(cis.name); + } } } } @@ -705,8 +707,6 @@ public class StaticData { return altCandidate; } - - /** * Get the Art Count for a given PaperCard looking for a candidate in all * available databases. @@ -734,7 +734,6 @@ public class StaticData { public void setMulliganRule(MulliganDefs.MulliganRule rule) { mulliganRule = rule; } - public MulliganDefs.MulliganRule getMulliganRule() { return mulliganRule; } @@ -754,7 +753,7 @@ public class StaticData { } public CardDb.CardArtPreference getCardArtPreference(boolean latestArt, boolean coreExpansionOnly) { - if (latestArt){ + if (latestArt) { return coreExpansionOnly ? CardDb.CardArtPreference.LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY : CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS; } return coreExpansionOnly ? CardDb.CardArtPreference.ORIGINAL_ART_CORE_EXPANSIONS_REPRINT_ONLY : CardDb.CardArtPreference.ORIGINAL_ART_ALL_EDITIONS; diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java index ce4bc826916..d07e65105b7 100644 --- a/forge-core/src/main/java/forge/card/CardDb.java +++ b/forge-core/src/main/java/forge/card/CardDb.java @@ -1037,7 +1037,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { } public PaperCard createUnsupportedCard(String cardRequest) { - CardRequest request = CardRequest.fromString(cardRequest); CardEdition cardEdition = CardEdition.UNKNOWN; CardRarity cardRarity = CardRarity.Unknown; @@ -1078,7 +1077,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool { } return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity); - } private final Editor editor = new Editor(); diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java index e57b780291f..065f7158acd 100644 --- a/forge-core/src/main/java/forge/card/CardRules.java +++ b/forge-core/src/main/java/forge/card/CardRules.java @@ -93,7 +93,7 @@ public final class CardRules implements ICardCharacteristics { 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() - switch(c) { + switch (c) { case('('): isReminder = i > 0; break; // if oracle has only reminder, consider it valid rules (basic and true lands need this) case(')'): isReminder = false; break; case('{'): isSymbol = true; break; @@ -132,7 +132,7 @@ public final class CardRules implements ICardCharacteristics { } public String getName() { - switch(splitType.getAggregationMethod()) { + switch (splitType.getAggregationMethod()) { case COMBINE: return mainPart.getName() + " // " + otherPart.getName(); default: @@ -149,7 +149,7 @@ public final class CardRules implements ICardCharacteristics { @Override public CardType getType() { - switch(splitType.getAggregationMethod()) { + switch (splitType.getAggregationMethod()) { case COMBINE: // no cards currently have different types return CardType.combine(mainPart.getType(), otherPart.getType()); default: @@ -159,7 +159,7 @@ public final class CardRules implements ICardCharacteristics { @Override public ManaCost getManaCost() { - switch(splitType.getAggregationMethod()) { + switch (splitType.getAggregationMethod()) { case COMBINE: return ManaCost.combine(mainPart.getManaCost(), otherPart.getManaCost()); default: @@ -169,7 +169,7 @@ public final class CardRules implements ICardCharacteristics { @Override public ColorSet getColor() { - switch(splitType.getAggregationMethod()) { + switch (splitType.getAggregationMethod()) { case COMBINE: return ColorSet.fromMask(mainPart.getColor().getColor() | otherPart.getColor().getColor()); default: @@ -186,7 +186,7 @@ public final class CardRules implements ICardCharacteristics { } public boolean canCastWithAvailable(byte colorCode) { - switch(splitType.getAggregationMethod()) { + switch (splitType.getAggregationMethod()) { case COMBINE: return canCastFace(mainPart, colorCode) || canCastFace(otherPart, colorCode); default: @@ -202,7 +202,7 @@ public final class CardRules implements ICardCharacteristics { @Override public String getOracleText() { - switch(splitType.getAggregationMethod()) { + switch (splitType.getAggregationMethod()) { case COMBINE: return mainPart.getOracleText() + "\r\n\r\n" + otherPart.getOracleText(); default: diff --git a/forge-core/src/main/java/forge/item/PaperCard.java b/forge-core/src/main/java/forge/item/PaperCard.java index d7adc209271..df10046f9b1 100644 --- a/forge-core/src/main/java/forge/item/PaperCard.java +++ b/forge-core/src/main/java/forge/item/PaperCard.java @@ -60,7 +60,7 @@ public final class PaperCard implements Comparable, InventoryItemFro // 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; + private transient PaperCard foiledVersion; @Override public String getName() { @@ -170,7 +170,7 @@ public final class PaperCard implements Comparable, InventoryItemFro } }; - public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0){ + public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0) { this(rules0, edition0, rarity0, IPaperCard.DEFAULT_ART_INDEX, false, IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME); } @@ -186,7 +186,7 @@ public final class PaperCard implements Comparable, InventoryItemFro artIndex = Math.max(artIndex0, IPaperCard.DEFAULT_ART_INDEX); foil = foil0; rarity = rarity0; - artist = artist0 != null ? TextUtil.normalizeText(artist0) : IPaperCard.NO_ARTIST_NAME; + artist = TextUtil.normalizeText(artist0); collectorNumber = (collectorNumber0 != null) && (collectorNumber0.length() > 0) ? collectorNumber0 : IPaperCard.NO_COLLECTOR_NUMBER; // If the user changes the language this will make cards sort by the old language until they restart the game. // This is a good tradeoff diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 16dda3f86b0..5a5f26146a1 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -349,36 +349,7 @@ public class AbilityUtils { cards.addAll(CardLists.getValidCards(candidates, validDefined, hostCard.getController(), hostCard, sa)); return cards; } else { - CardCollection list = null; - if (sa instanceof SpellAbility) { - SpellAbility root = ((SpellAbility)sa).getRootAbility(); - if (defined.startsWith("SacrificedCards")) { - list = root.getPaidList("SacrificedCards"); - } else if (defined.startsWith("Sacrificed")) { - list = root.getPaidList("Sacrificed"); - } else if (defined.startsWith("Revealed")) { - list = root.getPaidList("Revealed"); - } else if (defined.startsWith("DiscardedCards")) { - list = root.getPaidList("DiscardedCards"); - } else if (defined.startsWith("Discarded")) { - list = root.getPaidList("Discarded"); - } else if (defined.startsWith("ExiledCards")) { - list = root.getPaidList("ExiledCards"); - } else if (defined.startsWith("Exiled")) { - list = root.getPaidList("Exiled"); - } else if (defined.startsWith("Milled")) { - list = root.getPaidList("Milled"); - } else if (defined.startsWith("TappedCards")) { - list = root.getPaidList("TappedCards"); - } else if (defined.startsWith("Tapped")) { - list = root.getPaidList("Tapped"); - } else if (defined.startsWith("UntappedCards")) { - list = root.getPaidList("UntappedCards"); - } else if (defined.startsWith("Untapped")) { - list = root.getPaidList("Untapped"); - } - } - + CardCollection list = getPaidCards(sa, defined); if (list != null) { cards.addAll(list); } @@ -1004,7 +975,7 @@ public class AbilityUtils { final Player player = sa instanceof SpellAbility ? ((SpellAbility)sa).getActivatingPlayer() : card.getController(); - if (defined.equals("Self") || defined.equals("ThisTargetedCard")) { + if (defined.equals("Self") || defined.equals("ThisTargetedCard") || getPaidCards(sa, defined) != null) { // do nothing, Self is for Cards, not Players } else if (defined.equals("TargetedOrController")) { players.addAll(getDefinedPlayers(card, "Targeted", sa)); @@ -1225,10 +1196,8 @@ public class AbilityUtils { players.add(p); } } - else if (defined.equals("ChosenCardController")) { - for (final Card chosen : card.getChosenCards()) { - players.add(game.getCardState(chosen).getController()); - } + else if (defined.startsWith("ChosenCard")) { + addPlayer(Lists.newArrayList(card.getChosenCards()), defined, players); } else if (defined.equals("SourceController")) { players.add(sa.getHostCard().getController()); @@ -3864,6 +3833,39 @@ public class AbilityUtils { return someCards; } + public static CardCollection getPaidCards(CardTraitBase sa, String defined) { + CardCollection list = null; + if (sa instanceof SpellAbility) { + SpellAbility root = ((SpellAbility)sa).getRootAbility(); + if (defined.startsWith("SacrificedCards")) { + list = root.getPaidList("SacrificedCards"); + } else if (defined.startsWith("Sacrificed")) { + list = root.getPaidList("Sacrificed"); + } else if (defined.startsWith("Revealed")) { + list = root.getPaidList("Revealed"); + } else if (defined.startsWith("DiscardedCards")) { + list = root.getPaidList("DiscardedCards"); + } else if (defined.startsWith("Discarded")) { + list = root.getPaidList("Discarded"); + } else if (defined.startsWith("ExiledCards")) { + list = root.getPaidList("ExiledCards"); + } else if (defined.startsWith("Exiled")) { + list = root.getPaidList("Exiled"); + } else if (defined.startsWith("Milled")) { + list = root.getPaidList("Milled"); + } else if (defined.startsWith("TappedCards")) { + list = root.getPaidList("TappedCards"); + } else if (defined.startsWith("Tapped")) { + list = root.getPaidList("Tapped"); + } else if (defined.startsWith("UntappedCards")) { + list = root.getPaidList("UntappedCards"); + } else if (defined.startsWith("Untapped")) { + list = root.getPaidList("Untapped"); + } + } + return list; + } + public static int getNumberOfTypes(final Card card) { EnumSet types = EnumSet.noneOf(CardType.CoreType.class); Iterables.addAll(types, card.getType().getCoreTypes()); diff --git a/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java b/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java index 11de95a10b9..3011c5a9e28 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/CountersPutEffect.java @@ -133,6 +133,11 @@ public class CountersPutEffect extends SpellAbilityEffect { boolean existingCounter = sa.hasParam("CounterType") && sa.getParam("CounterType").equals("ExistingCounter"); boolean eachExistingCounter = sa.hasParam("EachExistingCounter"); + if (sa.hasParam("Optional") && !pc.confirmAction + (sa, null, Localizer.getInstance().getMessage("lblDoYouWantPutCounter"))) { + return; + } + List tgtObjects = Lists.newArrayList(); int divrem = 0; if (sa.hasParam("Bolster")) { @@ -198,11 +203,6 @@ public class CountersPutEffect extends SpellAbilityEffect { } } - if (sa.hasParam("Optional") - && !pc.confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantPutCounter"))) { - return; - } - int counterRemain = counterAmount; if (sa.hasParam("DividedRandomly")) { CardCollection targets = new CardCollection(); diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index d3fa3d8ea56..f3c737daa23 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -98,8 +98,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private CardState currentState; private CardStateName currentStateName = CardStateName.Original; - private Zone castFrom = null; - private SpellAbility castSA = null; + private Zone castFrom; + private SpellAbility castSA; private CardDamageHistory damageHistory = new CardDamageHistory(); // Hidden keywords won't be displayed on the card @@ -119,18 +119,18 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private Card encoding, cloneOrigin, haunting, effectSource, pairedWith, meldedWith; private Card mergedTo; - private SpellAbility effectSourceAbility = null; + private SpellAbility effectSourceAbility; - private GameEntity entityAttachedTo = null; + private GameEntity entityAttachedTo; - private GameEntity mustAttackEntity = null; - private GameEntity mustAttackEntityThisTurn = null; + private GameEntity mustAttackEntity; + private GameEntity mustAttackEntityThisTurn; private final Map mayPlay = Maps.newHashMap(); // changes by AF animate and continuous static effects - protected CardChangedType changedTypeByText = null; // Layer 3 by Text Change + protected CardChangedType changedTypeByText; // Layer 3 by Text Change // x=timestamp y=StaticAbility id private final Table changedCardTypesByText = TreeBasedTable.create(); // Layer 3 private final Table changedCardTypesCharacterDefining = TreeBasedTable.create(); // Layer 4 CDA @@ -193,22 +193,22 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private boolean sickness = true; // summoning sickness private boolean token = false; private boolean tokenCard = false; - private Card copiedPermanent = null; + private Card copiedPermanent; private boolean copiedSpell = false; private boolean canCounter = true; private boolean unearthed; - private boolean monstrous = false; + private boolean monstrous; - private boolean renowned = false; + private boolean renowned; - private boolean manifested = false; + private boolean manifested; - private boolean foretold = false; - private boolean foretoldThisTurn = false; - private boolean foretoldByEffect = false; + private boolean foretold; + private boolean foretoldThisTurn; + private boolean foretoldByEffect; private int timesCrewedThisTurn = 0; @@ -253,21 +253,21 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private String oracleText = ""; private int damage; - private boolean hasBeenDealtDeathtouchDamage = false; + private boolean hasBeenDealtDeathtouchDamage; // regeneration private FCollection shields = new FCollection<>(); - private int regeneratedThisTurn = 0; + private int regeneratedThisTurn; private int turnInZone; // the player that under which control it enters - private Player turnInController = null; + private Player turnInController; private Map xManaCostPaidByColor; - private Player owner = null; - private Player controller = null; - private long controllerTimestamp = 0; + private Player owner; + private Player controller; + private long controllerTimestamp; private NavigableMap tempControllers = Maps.newTreeMap(); private String originalText = "", text = ""; @@ -283,8 +283,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private String chosenMode = ""; private String currentRoom = null; - private Card exiledWith = null; - private Player exiledBy = null; + private Card exiledWith; + private Player exiledBy; private Map goad = Maps.newTreeMap(); @@ -298,11 +298,11 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private final List staticCommandList = Lists.newArrayList(); // Zone-changing spells should store card's zone here - private Zone currentZone = null; + private Zone currentZone; // LKI copies of cards are allowed to store the LKI about the zone the card was known to be in last. // For all cards except LKI copies this should always be null. - private Zone savedLastKnownZone = null; + private Zone savedLastKnownZone; // LKI copies of cards store CMC separately to avoid shenanigans with the game state visualization // breaking when the LKI object is changed to a different card state. private int lkiCMC = -1; @@ -314,7 +314,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private SpellAbility[] basicLandAbilities = new SpellAbility[MagicColor.WUBRG.length]; - private int planeswalkerAbilityActivated = 0; + private int planeswalkerAbilityActivated; private final ActivationTable numberTurnActivations = new ActivationTable(); private final ActivationTable numberGameActivations = new ActivationTable(); @@ -326,7 +326,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private final Table> chosenModesTurnStatic = HashBasedTable.create(); private final Table> chosenModesGameStatic = HashBasedTable.create(); - private CombatLki combatLKI = null; + private CombatLki combatLKI; // Enumeration for CMC request types public enum SplitCMCMode { @@ -3921,11 +3921,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { updatePTforView(); } - public final void addNewPT(final Integer power, final Integer toughness, final long timestamp, final long staticId) { addNewPT(power, toughness, timestamp, staticId, false); } - public final void addNewPT(final Integer power, final Integer toughness, final long timestamp, final long staticId, final boolean cda) { (cda ? newPTCharacterDefining : newPT).put(timestamp, staticId, Pair.of(power, toughness)); updatePTforView(); @@ -6135,7 +6133,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { public int getCMC() { return getCMC(SplitCMCMode.CurrentSideCMC); } - public int getCMC(SplitCMCMode mode) { if (isToken() && getCopiedPermanent() == null) { return 0; diff --git a/forge-game/src/main/java/forge/game/card/CardCloneStates.java b/forge-game/src/main/java/forge/game/card/CardCloneStates.java index a006a87d93e..796bf0e75ea 100644 --- a/forge-game/src/main/java/forge/game/card/CardCloneStates.java +++ b/forge-game/src/main/java/forge/game/card/CardCloneStates.java @@ -12,8 +12,8 @@ public class CardCloneStates extends ForwardingMap { private Map dataMap = Maps.newEnumMap(CardStateName.class); - private Card origin = null; - private CardTraitBase ctb = null; + private Card origin; + private CardTraitBase ctb; public CardCloneStates(Card origin, CardTraitBase sa) { super(); diff --git a/forge-game/src/main/java/forge/game/card/CardColor.java b/forge-game/src/main/java/forge/game/card/CardColor.java index 8bf0dae48ab..c37460a4128 100644 --- a/forge-game/src/main/java/forge/game/card/CardColor.java +++ b/forge-game/src/main/java/forge/game/card/CardColor.java @@ -27,7 +27,7 @@ import forge.card.ColorSet; * @author Forge * @version $Id$ */ -public class CardColor { +public class CardColor { private final byte colorMask; public final byte getColorMask() { return colorMask; diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index df0d3200f98..c93c39e4057 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -529,7 +529,7 @@ public class CardFactory { } public static void copySpellAbility(SpellAbility from, SpellAbility to, final Card host, final Player p, final boolean lki) { - if (from.getTargetRestrictions() != null) { + if (from.usesTargeting()) { to.setTargetRestrictions(from.getTargetRestrictions()); } to.setDescription(from.getOriginalDescription()); diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index f3e4cb7c365..6d267fca627 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -81,7 +81,7 @@ public class CardState extends GameObject implements IHasSVars { private final CardStateView view; private final Card card; - private ReplacementEffect loyaltyRep = null; + private ReplacementEffect loyaltyRep; public CardState(Card card, CardStateName name) { this(card.getView().createAlternateState(name), card); diff --git a/forge-game/src/main/java/forge/game/card/CardUtil.java b/forge-game/src/main/java/forge/game/card/CardUtil.java index f8f35a2c028..f4187a6a66a 100644 --- a/forge-game/src/main/java/forge/game/card/CardUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardUtil.java @@ -91,8 +91,7 @@ public final class CardUtil { kw = kw.substring(7); } - return !kw.startsWith("Protection") && !kw.startsWith("CantBeBlockedBy") - && !NON_STACKING_LIST.contains(kw); + return !kw.startsWith("Protection") && !NON_STACKING_LIST.contains(kw); } public static String getShortColorsString(final Iterable colors) { diff --git a/forge-game/src/main/java/forge/game/phase/Untap.java b/forge-game/src/main/java/forge/game/phase/Untap.java index 5a4bc3c4683..6542d1e3ab9 100644 --- a/forge-game/src/main/java/forge/game/phase/Untap.java +++ b/forge-game/src/main/java/forge/game/phase/Untap.java @@ -57,12 +57,12 @@ import forge.game.zone.ZoneType; public class Untap extends Phase { private static final long serialVersionUID = 4515266331266259123L; protected final Game game; - + public Untap(final Game game0) { super(PhaseType.UNTAP); game = game0; } - + /** *

* Executes any hardcoded triggers that happen "at end of combat". @@ -108,7 +108,7 @@ public class Untap extends Phase { return Untap.canUntap(c); } }; - + /** *

* doUntap. diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 49dcbf8959d..5280df05d0a 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -148,35 +148,37 @@ public class Player extends GameEntity implements Comparable { private int lifeStartedThisTurnWith = startingLife; private final Map assignedDamage = Maps.newHashMap(); private final Map assignedCombatDamage = Maps.newHashMap(); - private int spellsCastThisTurn = 0; - private int spellsCastThisGame = 0; - private int spellsCastLastTurn = 0; - private int landsPlayedThisTurn = 0; - private int landsPlayedLastTurn = 0; - private int investigatedThisTurn = 0; - private int surveilThisTurn = 0; - private int cycledThisTurn = 0; - private int equippedThisTurn = 0; - private int lifeLostThisTurn = 0; - private int lifeLostLastTurn = 0; - private int lifeGainedThisTurn = 0; - private int lifeGainedTimesThisTurn = 0; - private int lifeGainedByTeamThisTurn = 0; + private int spellsCastThisTurn; + private int spellsCastThisGame; + private int spellsCastLastTurn; + private int landsPlayedThisTurn; + private int landsPlayedLastTurn; + private int investigatedThisTurn; + private int surveilThisTurn; + private int cycledThisTurn; + private int equippedThisTurn; + private int lifeLostThisTurn; + private int lifeLostLastTurn; + private int lifeGainedThisTurn; + private int lifeGainedTimesThisTurn; + private int lifeGainedByTeamThisTurn; private int numPowerSurgeLands; - private int numLibrarySearchedOwn = 0; //The number of times this player has searched his library + private int numLibrarySearchedOwn; //The number of times this player has searched his library + private int numDrawnThisTurn; + private int numDrawnThisDrawStep; + private int numRollsThisTurn; + private int numDiscardedThisTurn; + private int numTokenCreatedThisTurn; + private int numForetoldThisTurn; + private int numCardsInHandStartedThisTurnWith; + private int attackersDeclaredThisTurn; + private int venturedThisTurn; private int maxHandSize = 7; private int startingHandSize = 7; private boolean unlimitedHandSize = false; - private Card lastDrawnCard = null; + private Card lastDrawnCard; private String namedCard = ""; private String namedCard2 = ""; - private int numDrawnThisTurn = 0; - private int numDrawnThisDrawStep = 0; - private int numRollsThisTurn = 0; - private int numDiscardedThisTurn = 0; - private int numTokenCreatedThisTurn = 0; - private int numForetoldThisTurn = 0; - private int numCardsInHandStartedThisTurnWith = 0; private int simultaneousDamage = 0; @@ -200,13 +202,11 @@ public class Player extends GameEntity implements Comparable { private Table changedKeywords = TreeBasedTable.create(); private ManaPool manaPool = new ManaPool(this); - private GameEntity mustAttackEntity = null; - private GameEntity mustAttackEntityThisTurn = null; + private GameEntity mustAttackEntity; + private GameEntity mustAttackEntityThisTurn; private CardCollection creatureAttackedThisTurn = new CardCollection(); private boolean activateLoyaltyAbilityThisTurn = false; private boolean tappedLandForManaThisTurn = false; - private int attackersDeclaredThisTurn = 0; - private int venturedThisTurn = 0; private List completedDungeons = new ArrayList<>(); private final Map zones = Maps.newEnumMap(ZoneType.class); @@ -236,9 +236,9 @@ public class Player extends GameEntity implements Comparable { // The SA currently being paid for private Deque paidForStack = new ArrayDeque<>(); - private Card monarchEffect = null; - private Card blessingEffect = null; - private Card keywordEffect = null; + private Card monarchEffect; + private Card blessingEffect; + private Card keywordEffect; private Map additionalVotes = Maps.newHashMap(); private Map additionalOptionalVotes = Maps.newHashMap(); diff --git a/forge-game/src/main/java/forge/game/spellability/AbilitySub.java b/forge-game/src/main/java/forge/game/spellability/AbilitySub.java index 43007a57a05..ceed6161705 100644 --- a/forge-game/src/main/java/forge/game/spellability/AbilitySub.java +++ b/forge-game/src/main/java/forge/game/spellability/AbilitySub.java @@ -39,7 +39,7 @@ public final class AbilitySub extends SpellAbility implements java.io.Serializab /** Constant serialVersionUID=4650634415821733134L. */ private static final long serialVersionUID = 4650634415821733134L; - private SpellAbility parent = null; + private SpellAbility parent; /** *

diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java index 4df71c13d17..0d106f6acc0 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbility.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbility.java @@ -110,20 +110,20 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit // choices for constructor isPermanent argument private String originalDescription = "", description = ""; private String originalStackDescription = "", stackDescription = ""; - private ManaCost multiKickerManaCost = null; - private Player activatingPlayer = null; - private Player targetingPlayer = null; - private Pair controlledByPlayer = null; - private ManaCostBeingPaid manaCostBeingPaid = null; + private ManaCost multiKickerManaCost; + private Player activatingPlayer; + private Player targetingPlayer; + private Pair controlledByPlayer; + private ManaCostBeingPaid manaCostBeingPaid; private boolean spentPhyrexian = false; - private SpellAbility grantorOriginal = null; - private StaticAbility grantorStatic = null; + private SpellAbility grantorOriginal; + private StaticAbility grantorStatic; private CardCollection splicedCards = null; private boolean basicSpell = true; - private Trigger triggerObj = null; + private Trigger triggerObj; private boolean optionalTrigger = false; private ReplacementEffect replacementEffect = null; private int sourceTrigger = -1; @@ -141,7 +141,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private Cost payCosts; private SpellAbilityRestriction restrictions = new SpellAbilityRestriction(); private SpellAbilityCondition conditions = new SpellAbilityCondition(); - private AbilitySub subAbility = null; + private AbilitySub subAbility; private Map additionalAbilities = Maps.newHashMap(); private Map> additionalAbilityLists = Maps.newHashMap(); @@ -160,10 +160,10 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private List chosenList = null; private CardCollection tappedForConvoke = new CardCollection(); - private Card sacrificedAsOffering = null; - private Card sacrificedAsEmerge = null; + private Card sacrificedAsOffering; + private Card sacrificedAsEmerge; - private AbilityManaPart manaPart = null; + private AbilityManaPart manaPart; private boolean undoable; @@ -171,36 +171,35 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit private boolean mayChooseNewTargets = false; private EnumSet optionalCosts = EnumSet.noneOf(OptionalCost.class); - private TargetRestrictions targetRestrictions = null; + private TargetRestrictions targetRestrictions; private TargetChoices targetChosen = new TargetChoices(); private Integer dividedValue = null; private SpellAbilityView view; - private StaticAbility mayPlay = null; + private StaticAbility mayPlay; private CardCollection lastStateBattlefield = null; private CardCollection lastStateGraveyard = null; private CardCollection rollbackEffects = new CardCollection(); - private CardDamageMap damageMap = null; - private CardDamageMap preventMap = null; - private GameEntityCounterTable counterTable = null; - private CardZoneTable changeZoneTable = null; + private CardDamageMap damageMap; + private CardDamageMap preventMap; + private GameEntityCounterTable counterTable; + private CardZoneTable changeZoneTable; public CardCollection getLastStateBattlefield() { return lastStateBattlefield; } - public void setLastStateBattlefield(final CardCollectionView lastStateBattlefield) { this.lastStateBattlefield = new CardCollection(lastStateBattlefield); } + public CardCollection getLastStateGraveyard() { return lastStateGraveyard; } - public void setLastStateGraveyard(final CardCollectionView lastStateGraveyard) { this.lastStateGraveyard = new CardCollection(lastStateGraveyard); } @@ -1876,7 +1875,7 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit final List targets = Lists.newArrayList(); SpellAbility child = getParent(); while (child != null) { - if (child.getTargetRestrictions() != null) { + if (child.usesTargeting()) { Iterables.addAll(targets, child.getTargets()); } child = child.getParent(); diff --git a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java index 336f8762c52..a566c4afc31 100644 --- a/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java +++ b/forge-game/src/main/java/forge/game/spellability/SpellAbilityStackInstance.java @@ -326,7 +326,7 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView { } while (compare != null && sub != null) { - TargetChoices choices = compare.getTargetRestrictions() != null ? compare.getTargets() : null; + TargetChoices choices = compare.usesTargeting() ? compare.getTargets() : null; if (choices != null && !choices.equals(sub.getTargetChoices())) { return false; diff --git a/forge-game/src/main/java/forge/game/spellability/TargetChoices.java b/forge-game/src/main/java/forge/game/spellability/TargetChoices.java index 565f306d8b4..0efe72d6cd2 100644 --- a/forge-game/src/main/java/forge/game/spellability/TargetChoices.java +++ b/forge-game/src/main/java/forge/game/spellability/TargetChoices.java @@ -45,7 +45,7 @@ import forge.util.collect.FCollection; */ public class TargetChoices extends ForwardingList implements Cloneable { - private final FCollection targets = new FCollection(); + private final FCollection targets = new FCollection<>(); private final Map dividedMap = Maps.newHashMap(); diff --git a/forge-game/src/main/java/forge/game/trigger/Trigger.java b/forge-game/src/main/java/forge/game/trigger/Trigger.java index 713da987d6a..498926da4fd 100644 --- a/forge-game/src/main/java/forge/game/trigger/Trigger.java +++ b/forge-game/src/main/java/forge/game/trigger/Trigger.java @@ -83,7 +83,7 @@ public abstract class Trigger extends TriggerReplacementBase { private Set validPhases; - private SpellAbility spawningAbility = null; + private SpellAbility spawningAbility; /** *

diff --git a/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java b/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java index ebc6ed5c4b0..b3c7c9419dc 100644 --- a/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java +++ b/forge-game/src/main/java/forge/game/trigger/TriggerAbilityTriggered.java @@ -79,7 +79,7 @@ public class TriggerAbilityTriggered extends Trigger { if (hasParam("ValidCause")) { boolean match = false; for (Card cause : causes) { - if(matchesValidParam("ValidCause", cause)) { + if (matchesValidParam("ValidCause", cause)) { match = true; } } diff --git a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java index 1a2a167ee15..dd8e8d006ed 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/match/CMatchUI.java @@ -1000,6 +1000,12 @@ public final class CMatchUI public void showPromptMessage(final PlayerView playerView, final String message) { cPrompt.setMessage(message); } + + @Override + public void showCardPromptMessage(PlayerView playerView, String message, CardView card) { + cPrompt.setMessage(message, card); + } + // no override for now public void showPromptMessage(final PlayerView playerView, final String message, final CardView card ) { cPrompt.setMessage(message,card); diff --git a/forge-gui-mobile/src/forge/Forge.java b/forge-gui-mobile/src/forge/Forge.java index 98960d81a4e..317643a6e32 100644 --- a/forge-gui-mobile/src/forge/Forge.java +++ b/forge-gui-mobile/src/forge/Forge.java @@ -339,7 +339,7 @@ public class Forge implements ApplicationListener { protected void afterDbLoaded() { //init here to fix crash if the assets are missing - transitionTexture = new Texture(GuiBase.isAndroid() ? Gdx.files.internal("fallback_skin").child("transition.png") : Gdx.files.classpath("fallback_skin").child("transition.png")); + transitionTexture = new Texture(Gdx.files.classpath("fallback_skin").child("transition.png")); destroyThis = false; //Allow back() diff --git a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java index be3a918b535..a181fb7984e 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java +++ b/forge-gui-mobile/src/forge/adventure/scene/AdventureDeckEditor.java @@ -38,7 +38,6 @@ import forge.toolbox.FLabel; import forge.toolbox.GuiChoose; import forge.util.Callback; import forge.util.ItemPool; -import forge.util.Localizer; import forge.util.Utils; import org.apache.commons.lang3.StringUtils; import org.checkerframework.checker.nullness.compatqual.NullableDecl; diff --git a/forge-gui-mobile/src/forge/adventure/scene/PlayerStatisticScene.java b/forge-gui-mobile/src/forge/adventure/scene/PlayerStatisticScene.java index 7fcee8d10dc..74efeed902c 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/PlayerStatisticScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/PlayerStatisticScene.java @@ -200,7 +200,6 @@ public class PlayerStatisticScene extends UIScene { back.getLabel().setText(Forge.getLocalizer().getMessage("lblBack")); ScrollPane scrollPane = ui.findActor("enemies"); scrollPane.setActor(enemiesGroup); - enemiesGroup.setFillParent(true); this.init = true; } } diff --git a/forge-gui-mobile/src/forge/adventure/scene/SaveLoadScene.java b/forge-gui-mobile/src/forge/adventure/scene/SaveLoadScene.java index 3bdfc1c50ad..1b3690bd9c8 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/SaveLoadScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/SaveLoadScene.java @@ -54,7 +54,7 @@ public class SaveLoadScene extends UIScene { private TextButton addSaveSlot(String name, int i) { - layout.add(Controls.newLabel(name)).align(Align.left).pad(4, 10, 4, 15); + layout.add(Controls.newLabel(name)).align(Align.left).pad(2, 5, 2, 10); TextButton button = Controls.newTextButton("..."); button.addListener(new ClickListener() { @Override @@ -213,7 +213,6 @@ public class SaveLoadScene extends UIScene { super.resLoaded(); if (!this.init) { layout = new Table(); - layout.setFillParent(true); stage.addActor(layout); dialog = Controls.newDialog(Forge.getLocalizer().getMessage("lblSave")); textInput = Controls.newTextField(""); @@ -257,9 +256,8 @@ public class SaveLoadScene extends UIScene { previewImage = ui.findActor("preview"); previewBorder = ui.findActor("preview_border"); header = Controls.newLabel(Forge.getLocalizer().getMessage("lblSave")); - header.setHeight(header.getHeight() * 2); header.setAlignment(Align.center); - layout.add(header).pad(2).colspan(4).align(Align.center).expand(); + layout.add(header).pad(2).colspan(4).align(Align.center).expandX(); layout.row(); autoSave = addSaveSlot(Forge.getLocalizer().getMessage("lblAutoSave"), WorldSave.AUTO_SAVE_SLOT); quickSave = addSaveSlot(Forge.getLocalizer().getMessage("lblQuickSave"), WorldSave.QUICK_SAVE_SLOT); diff --git a/forge-gui-mobile/src/forge/assets/AssetsDownloader.java b/forge-gui-mobile/src/forge/assets/AssetsDownloader.java index 717028977a3..02ece668d82 100644 --- a/forge-gui-mobile/src/forge/assets/AssetsDownloader.java +++ b/forge-gui-mobile/src/forge/assets/AssetsDownloader.java @@ -74,8 +74,8 @@ public class AssetsDownloader { } return; } - SOptionPane.showMessageDialog("Could not download update. " + - "Press OK to proceed without update.", "Update Failed"); + SOptionPane.showOptionDialog("Could not download update. " + + "Press OK to proceed without update.", "Update Failed", null, ImmutableList.of("Ok")); } } } @@ -112,9 +112,12 @@ public class AssetsDownloader { else { message += "You cannot start the app since you haven't previously downloaded these files."; } - SOptionPane.showMessageDialog(message, "No Internet Connection"); - if (!canIgnoreDownload) { - Forge.exitAnimation(false); //exit if can't ignore download + switch (SOptionPane.showOptionDialog(message, "No Internet Connection", null, ImmutableList.of("Ok"))) { + default: { + if (!canIgnoreDownload) { + Forge.exitAnimation(false); //exit if can't ignore download + } + } } return; } diff --git a/forge-gui-mobile/src/forge/assets/FSkin.java b/forge-gui-mobile/src/forge/assets/FSkin.java index 8f7abc78286..46f4619829d 100644 --- a/forge-gui-mobile/src/forge/assets/FSkin.java +++ b/forge-gui-mobile/src/forge/assets/FSkin.java @@ -108,7 +108,7 @@ public class FSkin { { if (!dir.exists() || !dir.isDirectory()) { //if skins directory doesn't exist, point to internal assets/skin directory instead for the sake of the splash screen - preferredDir = GuiBase.isAndroid() ? Gdx.files.internal("fallback_skin") : Gdx.files.classpath("fallback_skin"); + preferredDir = Gdx.files.classpath("fallback_skin"); } else { if (splashScreen != null) { diff --git a/forge-gui-mobile/src/forge/screens/SplashScreen.java b/forge-gui-mobile/src/forge/screens/SplashScreen.java index 958fa78dbb1..ce5b40db88f 100644 --- a/forge-gui-mobile/src/forge/screens/SplashScreen.java +++ b/forge-gui-mobile/src/forge/screens/SplashScreen.java @@ -97,9 +97,7 @@ public class SplashScreen extends FContainer { private float progress = 0; private boolean finished, openAdventure; //for transition image only... - TextureRegion transition_bg = new TextureRegion(new Texture(GuiBase.isAndroid() - ? Gdx.files.internal("fallback_skin").child("title_bg_lq.png") - : Gdx.files.classpath("fallback_skin").child("title_bg_lq.png"))); + TextureRegion transition_bg = new TextureRegion(new Texture(Gdx.files.classpath("fallback_skin").child("title_bg_lq.png"))); public void drawBackground(Graphics g) { float percentage = progress / DURATION; diff --git a/forge-gui-mobile/src/forge/screens/match/MatchController.java b/forge-gui-mobile/src/forge/screens/match/MatchController.java index 55cee891644..6773711bed2 100644 --- a/forge-gui-mobile/src/forge/screens/match/MatchController.java +++ b/forge-gui-mobile/src/forge/screens/match/MatchController.java @@ -12,6 +12,7 @@ import forge.deck.Deck; import forge.game.player.Player; import forge.item.IPaperCard; import forge.screens.TransitionScreen; +import forge.util.collect.FCollection; import org.apache.commons.lang3.StringUtils; import com.google.common.base.Function; @@ -157,13 +158,18 @@ public class MatchController extends AbstractGuiGame { public void openView(final TrackableCollection myPlayers) { final boolean noHumans = !hasLocalPlayers(); - final FCollectionView allPlayers = getGameView().getPlayers(); + FCollectionView players = getGameView().getPlayers(); + if (players.size() == 2 && myPlayers != null && myPlayers.size() == 1 && myPlayers.get(0).equals(players.get(1))) { + players = new FCollection<>(new PlayerView[]{players.get(1), players.get(0)}); + } final List playerPanels = new ArrayList<>(); - for (final PlayerView p : allPlayers) { + boolean init = false; + for (final PlayerView p : players) { final boolean isLocal = isLocalPlayer(p); - final VPlayerPanel playerPanel = new VPlayerPanel(p, isLocal || noHumans, allPlayers.size()); - if (isLocal && !playerPanels.isEmpty()) { + final VPlayerPanel playerPanel = new VPlayerPanel(p, isLocal || noHumans, players.size()); + if (isLocal && !init) { playerPanels.add(0, playerPanel); //ensure local player always first among player panels + init = true; } else { playerPanels.add(playerPanel); @@ -203,7 +209,7 @@ public class MatchController extends AbstractGuiGame { } @Override - public void showPromptMessage(final PlayerView player, final String message, final CardView card) { + public void showCardPromptMessage(final PlayerView player, final String message, final CardView card) { view.getPrompt(player).setMessage(message, card); } diff --git a/forge-gui-mobile/src/forge/screens/quest/QuestDuelsScreen.java b/forge-gui-mobile/src/forge/screens/quest/QuestDuelsScreen.java index 44f4a1760e3..52a6757d73a 100644 --- a/forge-gui-mobile/src/forge/screens/quest/QuestDuelsScreen.java +++ b/forge-gui-mobile/src/forge/screens/quest/QuestDuelsScreen.java @@ -12,7 +12,6 @@ import forge.gui.FThreads; import forge.gui.interfaces.IButton; import forge.model.FModel; import forge.screens.LoadingOverlay; -import forge.screens.home.HomeScreen; import forge.toolbox.FEvent; import forge.toolbox.FEvent.FEventHandler; import forge.toolbox.FLabel; diff --git a/forge-gui-mobile/src/forge/toolbox/FCardPanel.java b/forge-gui-mobile/src/forge/toolbox/FCardPanel.java index 195990253d5..10a33b9cb8e 100644 --- a/forge-gui-mobile/src/forge/toolbox/FCardPanel.java +++ b/forge-gui-mobile/src/forge/toolbox/FCardPanel.java @@ -112,7 +112,7 @@ public class FCardPanel extends FDisplayObject { return; } - if (!animate || MatchController.instance.isGameFast() || MatchController.instance.getGameView().isMatchOver()) { + if (!animate || MatchController.instance.isGameFast() || (MatchController.instance.getGameView() != null && MatchController.instance.getGameView().isMatchOver())) { //don't animate if game is fast or match is over rotateTransform(g, x, y, w, h, edgeOffset, false); card.updateNeedsTapAnimation(false); diff --git a/forge-gui/release-files/ANNOUNCEMENTS.txt b/forge-gui/release-files/ANNOUNCEMENTS.txt index a7a300dfb19..fb9a37eb223 100644 --- a/forge-gui/release-files/ANNOUNCEMENTS.txt +++ b/forge-gui/release-files/ANNOUNCEMENTS.txt @@ -1,6 +1,5 @@ #Add one announcement per line Get in the discord if you aren't yet. https://discord.gg/3v9JCVr -All new Alchemy: Innistrad cards (Y22) have been implemented in Forge. Happy brewing! -Support for rebalanced Arena cards (separate from the original implementations) is implemented. -It's now possible to choose to play Constructed matches in the best of 1, 3, and 5 formats. +Kamigawa: Neon Dynasty (NEO) and Kamigawa: Neon Dynasty Commander (NEC) are fully implemented. +Mobile Forge and Mobile backports now integrate Adventure Mode, an overworld adventure in the spirit of Shandalar. *** Android 7 & 8 support is now deprecated. Support will be dropped in an upcoming release. *** diff --git a/forge-gui/res/cardsfolder/s/swallow_whole.txt b/forge-gui/res/cardsfolder/s/swallow_whole.txt index 451568bfd89..e0c2dd1596c 100644 --- a/forge-gui/res/cardsfolder/s/swallow_whole.txt +++ b/forge-gui/res/cardsfolder/s/swallow_whole.txt @@ -2,6 +2,6 @@ Name:Swallow Whole ManaCost:W Types:Sorcery A:SP$ ChangeZone | Cost$ W tapXType<1/Creature> | ValidTgts$ Creature.tapped | TgtPrompt$ Select target tapped creature | Origin$ Battlefield | Destination$ Exile | SubAbility$ DBPutCounter | SpellDescription$ As an additional cost to cast this spell, tap an untapped creature you control. Exile target tapped creature. -SVar:DBPutCounter:DB$ PutCounter | Defined$ Tapped | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on the creature tapped to cast this spell. +SVar:DBPutCounter:DB$ PutCounter | Defined$ Tapped | CounterType$ P1P1 | SpellDescription$ Put a +1/+1 counter on the creature tapped to cast this spell. DeckHas:Ability$Counters Oracle:As an additional cost to cast this spell, tap an untapped creature you control.\nExile target tapped creature. Put a +1/+1 counter on the creature tapped to pay this spell's additional cost. diff --git a/forge-gui/res/editions/Pro Tour Promos.txt b/forge-gui/res/editions/Pro Tour Promos.txt index 8fe421fd4bb..e553340dbb2 100644 --- a/forge-gui/res/editions/Pro Tour Promos.txt +++ b/forge-gui/res/editions/Pro Tour Promos.txt @@ -34,3 +34,6 @@ ScryfallCode=PPRO 2035 U Saw it Coming @Anato Finnstark 2036 M Kaya the Inexorable @Jason A. Engle 2037 R Faceless Haven @Pablo Mendoza +2038 M Grand Master of Flowers +2039 R Adult Gold Dragon +2040 U Krydle of Baldur's Gate diff --git a/forge-gui/res/formats/Casual/Pauper.txt b/forge-gui/res/formats/Casual/Pauper.txt index b7f46f19636..cda60c6715c 100644 --- a/forge-gui/res/formats/Casual/Pauper.txt +++ b/forge-gui/res/formats/Casual/Pauper.txt @@ -4,4 +4,4 @@ Order:108 Subtype:Custom Type:Casual Rarities:L, C -Banned:Arcum's Astrolabe; Atog; Bonder's Ornament; Chatterstorm; Cloud of Faeries; Cloudpost; Cranial Plating; Daze; Expedition Map; Empty the Warrens; Fall from Favor; Frantic Search; Gitaxian Probe; Grapeshot; Gush; High Tide; Hymn to Tourach; Invigorate; Mystic Sanctuary; Peregrine Drake; Prophetic Prism; Sinkhole; Sojourner's Companion; Temporal Fissure; Treasure Cruise +Banned:Arcum's Astrolabe; Atog; Bonder's Ornament; Chatterstorm; Cloud of Faeries; Cloudpost; Cranial Plating; Daze; Disciple of the Vault; Empty the Warrens; Fall from Favor; Frantic Search; Galvanic Relay; Gitaxian Probe; Grapeshot; Gush; High Tide; Hymn to Tourach; Invigorate; Mystic Sanctuary; Peregrine Drake; Prophetic Prism; Sinkhole; Sojourner's Companion; Temporal Fissure; Treasure Cruise diff --git a/forge-gui/res/formats/Sanctioned/Modern.txt b/forge-gui/res/formats/Sanctioned/Modern.txt index 945a1300f9d..f2b1ff94c2b 100644 --- a/forge-gui/res/formats/Sanctioned/Modern.txt +++ b/forge-gui/res/formats/Sanctioned/Modern.txt @@ -4,4 +4,4 @@ Order:103 Subtype:Modern Type:Sanctioned Sets:8ED, MRD, DST, 5DN, CHK, BOK, SOK, 9ED, RAV, GPT, DIS, CSP, TSP, TSB, TSR, PLC, FUT, 10E, LRW, EVE, SHM, MOR, ALA, CFX, ARB, M10, ZEN, WWK, ROE, M11, SOM, MBS, NPH, M12, ISD, DKA, AVR, M13, RTR, GTC, DGM, M14, THS, BNG, JOU, M15, KTK, FRF, DTK, MMA, MM2, MM3, ORI, BFZ, OGW, SOI, EMN, KLD, AER, AKH, W16, W17, HOU, XLN, RIX, DOM, M19, G18, GRN, RNA, WAR, MH1, M20, ELD, THB, IKO, M21, ZNR, KHM, STX, MH2, AFR, MID, VOW, NEO -Banned:Ancient Den; Arcum's Astrolabe; Birthing Pod; Blazing Shoal; Bridge from Below; Chrome Mox; Cloudpost; Dark Depths; Deathrite Shaman; Dig Through Time; Dread Return; Eye of Ugin; Faithless Looting; Field of the Dead; Gitaxian Probe; Glimpse of Nature; Golgari Grave-Troll; Great Furnace; Green Sun's Zenith; Hogaak, Arisen Necropolis; Hypergenesis; Krark-Clan Ironworks; Mental Misstep; Mox Opal; Mycosynth Lattice; Mystic Sanctuary; Oko, Thief of Crowns; Once Upon A Time; Ponder; Preordain; Punishing Fire; Rite of Flame; Seat of the Synod; Second Sunrise; Seething Song; Sensei's Divining Top; Simian Spirit Guide; Skullclamp; Splinter Twin; Summer Bloom; Tibalt's Trickery; Treasure Cruise; Tree of Tales; Umezawa's Jitte; Uro, Titan of Nature's Wrath; Vault of Whispers +Banned:Ancient Den; Arcum's Astrolabe; Birthing Pod; Blazing Shoal; Bridge from Below; Chrome Mox; Cloudpost; Dark Depths; Deathrite Shaman; Dig Through Time; Dread Return; Eye of Ugin; Faithless Looting; Field of the Dead; Gitaxian Probe; Glimpse of Nature; Golgari Grave-Troll; Great Furnace; Green Sun's Zenith; Hogaak, Arisen Necropolis; Hypergenesis; Krark-Clan Ironworks; Lurrus of the Dream-Den; Mental Misstep; Mox Opal; Mycosynth Lattice; Mystic Sanctuary; Oko, Thief of Crowns; Once Upon A Time; Ponder; Preordain; Punishing Fire; Rite of Flame; Seat of the Synod; Second Sunrise; Seething Song; Sensei's Divining Top; Simian Spirit Guide; Skullclamp; Splinter Twin; Summer Bloom; Tibalt's Trickery; Treasure Cruise; Tree of Tales; Umezawa's Jitte; Uro, Titan of Nature's Wrath; Vault of Whispers diff --git a/forge-gui/res/formats/Sanctioned/Pioneer.txt b/forge-gui/res/formats/Sanctioned/Pioneer.txt index a5c2bedccda..89fa22da0aa 100644 --- a/forge-gui/res/formats/Sanctioned/Pioneer.txt +++ b/forge-gui/res/formats/Sanctioned/Pioneer.txt @@ -4,4 +4,4 @@ Order:102 Subtype:Pioneer Type:Sanctioned Sets:RTR, GTC, DGM, M14, THS, BNG, JOU, M15, KTK, FRF, DTK, ORI, BFZ, OGW, SOI, EMN, KLD, AER, AKH, HOU, XLN, RIX, DOM, M19, G18, GRN, RNA, WAR, M20, ELD, THB, IKO, M21, ZNR, KHM, STX, AFR, MID, VOW, NEO -Banned:Balustrade Spy; Bloodstained Mire; Felidar Guardian; Field of the Dead; Flooded Strand; Inverter of Truth; Kethis, the Hidden Hand; Leyline of Abundance; Nexus of Fate; Oko, Thief of Crowns; Once Upon a Time; Polluted Delta; Smuggler's Copter; Teferi, Time Raveler; Undercity Informer; Underworld Breach; Uro, Titan of Nature's Wrath; Veil of Summer; Walking Ballista; Wilderness Reclamation; Windswept Heath; Wooded Foothills +Banned:Balustrade Spy; Bloodstained Mire; Felidar Guardian; Field of the Dead; Flooded Strand; Inverter of Truth; Kethis, the Hidden Hand; Leyline of Abundance; Lurrus of the Dream-Den; Nexus of Fate; Oko, Thief of Crowns; Once Upon a Time; Polluted Delta; Smuggler's Copter; Teferi, Time Raveler; Undercity Informer; Underworld Breach; Uro, Titan of Nature's Wrath; Veil of Summer; Walking Ballista; Wilderness Reclamation; Windswept Heath; Wooded Foothills diff --git a/forge-gui/res/languages/de-DE.properties b/forge-gui/res/languages/de-DE.properties index 9a087f18298..ad28d28946c 100644 --- a/forge-gui/res/languages/de-DE.properties +++ b/forge-gui/res/languages/de-DE.properties @@ -2690,9 +2690,9 @@ lblUseFormatFilter=Wähle ein Format für die Deckliste lblIgnoreBnR=Importiere auch gebannte und eingeschränkte Karten ttIgnoreBnR=Wenn aktiviert, erden auch gebannte oder eingeschränkte Karten ins Deck importiert. nlIgnoreBnR=Warnung: Das Deck kann bei eingeschalteter Deckkonkonformität nicht spielbar sein. -lblUseSmartCardArt=Enable Smart Card Art Selection -ttUseSmartCardArtNoDeck=If enabled, the art of cards will be automatically chosen to match up with other cards in the Decklist. -ttUseSmartCardArtWithDeck=If enabled, the art of cards will be automatically chosen to match up with other cards in the Decklist, and in current Deck. +lblUseSmartCardArt=Aktiviere intelligente Kartenbildwahl +ttUseSmartCardArtNoDeck=Wenn aktiviert, wird das Kartenbild automatisch passend zu anderen Karten in der Deckliste gewählt. +ttUseSmartCardArtWithDeck=Wenn aktiviert, wird das Kartenbild automatisch passend zu anderen Karten in der Deckliste und dem aktuellen Deck gewählt. lblExtraOptions=Zeige Optionen lblHideOptions=Verstecke Optionen lblCardPreview=Karten-Vorschau @@ -2875,6 +2875,6 @@ lblAbort=Abbrechen lblNameYourSaveFile=Nennen Sie Ihre neue Save-Datei lblEdit=Bearbeiten lblWinProper=Sieg -lblLossProper=Verlust -lblWinLossRatio=Verlustquote gewinnen -lblHeal=Heilen \ No newline at end of file +lblLossProper=Niederlage +lblWinLossRatio=Sieg/Niederlage-Quote +lblHeal=Heilen diff --git a/forge-gui/res/lists/net-decks.txt b/forge-gui/res/lists/net-decks.txt index 67605a5c537..80a65efe46e 100644 --- a/forge-gui/res/lists/net-decks.txt +++ b/forge-gui/res/lists/net-decks.txt @@ -6,6 +6,10 @@ Budget Modern Metagame | https://downloads.cardforge.org/decks/budgetmodernmetag Building on a Budget | https://downloads.cardforge.org/decks/buildingonabudget.zip Card Preview | https://downloads.cardforge.org/decks/cardpreview.zip Community Cup | https://downloads.cardforge.org/decks/communitycup.zip +Current Alchemy Metagame | https://downloads.cardforge.org/decks/currentalchemymetagame.zip +Current Bo1 Alchemy Metagame | https://downloads.cardforge.org/decks/currentbo1alchemymetagame.zip +Current Bo1 Historic Metagame | https://downloads.cardforge.org/decks/currentbo1historicmetagame.zip +Current Bo1 Standard Metagame | https://downloads.cardforge.org/decks/currentbo1standardmetagame.zip Current Historic Metagame | https://downloads.cardforge.org/decks/currenthistoricmetagame.zip Current Legacy Metagame | https://downloads.cardforge.org/decks/currentlegacymetagame.zip Current Modern Metagame | https://downloads.cardforge.org/decks/currentmodernmetagame.zip diff --git a/forge-gui/res/puzzle/PS_NEO1.pzl b/forge-gui/res/puzzle/PS_NEO1.pzl new file mode 100644 index 00000000000..712c8f0b35b --- /dev/null +++ b/forge-gui/res/puzzle/PS_NEO1.pzl @@ -0,0 +1,17 @@ +[metadata] +Name:Possibility Storm - Kamigawa: Neon Dynasty #01 +URL:https://i2.wp.com/www.possibilitystorm.com/wp-content/uploads/2022/01/latest-3-scaled.jpg?ssl=1 +Goal:Win +Turns:1 +Difficulty:Uncommon +Description:Win this turn. The top card of your library is Bloodthirsty Adversary. Assume any other cards you could access are irrelevant to the solution. Good luck! +[state] +humanlife=20 +ailife=11 +turn=1 +activeplayer=human +activephase=MAIN1 +humanhand=Hero's Downfall;The Meathook Massacre;Kappa Tech-Wrecker +humanlibrary=Bloodthirsty Adversary;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt;Opt +humanbattlefield=Bookwurm;Atsushi, the Blazing Sky;Hidetsugu, Devouring Chaos;Swamp;Swamp;Swamp;Swamp;Mountain;Mountain;Mountain;Forest +aibattlefield=Angel of Destiny;Henrika Domnathi|Transformed;Vengeful Reaper diff --git a/forge-gui/res/puzzle/PS_NEO2.pzl b/forge-gui/res/puzzle/PS_NEO2.pzl new file mode 100644 index 00000000000..bd580b49f21 --- /dev/null +++ b/forge-gui/res/puzzle/PS_NEO2.pzl @@ -0,0 +1,17 @@ +[metadata] +Name:Possibility Storm - Kamigawa: Neon Dynasty #02 +URL:https://i0.wp.com/www.possibilitystorm.com/wp-content/uploads/2022/02/latest-scaled.jpg?ssl=1 +Goal:Win +Turns:1 +Difficulty:Mythic +Description:Win this turn. Start in your first main phase (Saga has resolved). Assume opponent has no mana. +[state] +humanlife=4 +ailife=8 +turn=1 +activeplayer=human +activephase=MAIN1 +humanhand=Jugan Defends the Temple;Satsuki, the Living Lore;The Kami War;Fall of the Impostor;Croaking Counterpart +humanbattlefield=Battle for Bretagard|Counters:LORE=2;Tuktuk Rubblefort;Overgrown Farmland;Overgrown Farmland;Overgrown Farmland;Overgrown Farmland;Vineglimmer Snarl;Vineglimmer Snarl;Vineglimmer Snarl;The World Tree +aibattlefield=Flamescroll Celebrant;Colossal Skyturtle;Weaver of Harmony;Greater Tanuki +humanprecast=Battle for Bretagard:TrigToken1;Battle for Bretagard:TrigToken2 diff --git a/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java b/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java index 5a52d579371..a6e10c3d992 100644 --- a/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java +++ b/forge-gui/src/main/java/forge/gamemodes/match/input/InputBase.java @@ -123,10 +123,10 @@ public abstract class InputBase implements java.io.Serializable, Input { controller.getGui().showPromptMessage(getOwner(), message); } protected final void showMessage(final String message, final SpellAbilityView sav) { - controller.getGui().showPromptMessage(getOwner(), message, sav.getHostCard()); + controller.getGui().showCardPromptMessage(getOwner(), message, sav.getHostCard()); } protected final void showMessage(final String message, final CardView card) { - controller.getGui().showPromptMessage(getOwner(), message, card); + controller.getGui().showCardPromptMessage(getOwner(), message, card); } protected String getTurnPhasePriorityMessage(final Game game) { diff --git a/forge-gui/src/main/java/forge/gamemodes/net/CObjectInputStream.java b/forge-gui/src/main/java/forge/gamemodes/net/CObjectInputStream.java index 9a7303230df..95d21c52637 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/CObjectInputStream.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/CObjectInputStream.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; -import java.io.StreamCorruptedException; import io.netty.handler.codec.serialization.ClassResolver; @@ -23,16 +22,19 @@ public class CObjectInputStream extends ObjectInputStream { if (type < 0) { throw new EOFException(); } else { - switch(type) { - case 0: - return super.readClassDescriptor(); - case 1: - String className = readUTF(); - Class clazz = classResolver.resolve(className); - return ObjectStreamClass.lookupAny(clazz); - default: - throw new StreamCorruptedException("Unexpected class descriptor type: " + type); + ObjectStreamClass resultClassDescriptor = super.readClassDescriptor(); + Class localClass; + try { + localClass = Class.forName(resultClassDescriptor.getName()); + } catch (ClassNotFoundException e) { + System.err.println("[Class Not Found Exception]\nNo local class for " + resultClassDescriptor.getName()); + return resultClassDescriptor; } + ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookupAny(localClass); + if (localClassDescriptor != null && type == 1) { + resultClassDescriptor = localClassDescriptor; // Use local class descriptor for deserialization by default + } + return resultClassDescriptor; } } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java b/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java index e29adcf0095..e8bf8957670 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/ProtocolMethod.java @@ -32,21 +32,22 @@ public enum ProtocolMethod { // Server -> Client setGameView (Mode.SERVER, Void.TYPE, GameView.class), openView (Mode.SERVER, Void.TYPE, TrackableCollection/*PlayerView*/.class), - afterGameEnd (Mode.SERVER), - showCombat (Mode.SERVER), + afterGameEnd (Mode.SERVER, Void.TYPE), + showCombat (Mode.SERVER, Void.TYPE), showPromptMessage (Mode.SERVER, Void.TYPE, PlayerView.class, String.class), + showCardPromptMessage (Mode.SERVER, Void.TYPE, PlayerView.class, String.class, CardView.class), updateButtons (Mode.SERVER, Void.TYPE, PlayerView.class, String.class, String.class, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE), - flashIncorrectAction(Mode.SERVER), - alertUser (Mode.SERVER), + flashIncorrectAction(Mode.SERVER, Void.TYPE), + alertUser (Mode.SERVER, Void.TYPE), updatePhase (Mode.SERVER, Void.TYPE, Boolean.TYPE), updateTurn (Mode.SERVER, Void.TYPE, PlayerView.class), - updatePlayerControl (Mode.SERVER), - enableOverlay (Mode.SERVER), - disableOverlay (Mode.SERVER), - finishGame (Mode.SERVER), + updatePlayerControl (Mode.SERVER, Void.TYPE), + enableOverlay (Mode.SERVER, Void.TYPE), + disableOverlay (Mode.SERVER, Void.TYPE), + finishGame (Mode.SERVER, Void.TYPE), showManaPool (Mode.SERVER, Void.TYPE, PlayerView.class), hideManaPool (Mode.SERVER, Void.TYPE, PlayerView.class), - updateStack (Mode.SERVER), + updateStack (Mode.SERVER, Void.TYPE), updateZones (Mode.SERVER, Void.TYPE, Iterable/*PlayerZoneUpdate*/.class), tempShowZones (Mode.SERVER, Iterable/*PlayerZoneUpdate*/.class, PlayerView.class, Iterable/*PlayerZoneUpdate*/.class), hideZones (Mode.SERVER, Void.TYPE, PlayerView.class, Iterable/*PlayerZoneUpdate*/.class), @@ -71,11 +72,11 @@ public enum ProtocolMethod { manipulateCardList (Mode.SERVER, List.class, String.class, Iterable.class, Iterable.class, Boolean.TYPE, Boolean.TYPE, Boolean.TYPE), setCard (Mode.SERVER, Void.TYPE, CardView.class), setSelectables (Mode.SERVER, Void.TYPE, Iterable/*CardView*/.class), - clearSelectables (Mode.SERVER), - refreshField (Mode.SERVER), + clearSelectables (Mode.SERVER, Void.TYPE), + refreshField (Mode.SERVER, Void.TYPE), // TODO case "setPlayerAvatar": openZones (Mode.SERVER, PlayerZoneUpdates.class, PlayerView.class, Collection/*ZoneType*/.class, Map/*PlayerView,Object*/.class), - restoreOldZones (Mode.SERVER, Void.TYPE, PlayerView.class, PlayerZoneUpdates.class), + restoreOldZones (Mode.SERVER, Void.TYPE, PlayerView.class, Iterable/*PlayerZoneUpdates*/.class), isUiSetToSkipPhase (Mode.SERVER, Boolean.TYPE, PlayerView.class, PhaseType.class), setRememberedActions(Mode.SERVER, Void.TYPE), nextRememberedAction(Mode.SERVER, Void.TYPE), @@ -85,18 +86,18 @@ public enum ProtocolMethod { // which client and server wait for one another's response and block // the threads that're supposed to give that response useMana (Mode.CLIENT, Void.TYPE, Byte.TYPE), - undoLastAction (Mode.CLIENT, Void.TYPE, Boolean.TYPE), + undoLastAction (Mode.CLIENT, Void.TYPE), selectPlayer (Mode.CLIENT, Void.TYPE, PlayerView.class, ITriggerEvent.class), selectCard (Mode.CLIENT, Void.TYPE, CardView.class, List.class, ITriggerEvent.class), - selectButtonOk (Mode.CLIENT), - selectButtonCancel (Mode.CLIENT), + selectButtonOk (Mode.CLIENT, Void.TYPE), + selectButtonCancel (Mode.CLIENT, Void.TYPE), selectAbility (Mode.CLIENT, Void.TYPE, SpellAbilityView.class), - passPriorityUntilEndOfTurn(Mode.CLIENT), - passPriority (Mode.CLIENT), + passPriorityUntilEndOfTurn(Mode.CLIENT, Void.TYPE), + passPriority (Mode.CLIENT, Void.TYPE), nextGameDecision (Mode.CLIENT, Void.TYPE, NextGameDecision.class), getActivateDescription (Mode.CLIENT, String.class, CardView.class), - concede (Mode.CLIENT), - alphaStrike (Mode.CLIENT), + concede (Mode.CLIENT, Void.TYPE), + alphaStrike (Mode.CLIENT, Void.TYPE), reorderHand (Mode.CLIENT, Void.TYPE, CardView.class, Integer.TYPE); private enum Mode { @@ -161,13 +162,17 @@ public enum ProtocolMethod { if(!GuiBase.hasPropertyConfig()) return; //if the experimental network option is enabled, then check the args, else let the default decoder handle it - for (int iArg = 0; iArg < args.length; iArg++) { - final Object arg = args[iArg]; - final Class type = this.args[iArg]; - if (!ReflectionUtil.isInstance(arg, type)) { - //throw new InternalError(String.format("Protocol method %s: illegal argument (%d) of type %s, %s expected", name(), iArg, arg.getClass().getName(), type.getName())); - System.err.println(String.format("InternalError: Protocol method %s: illegal argument (%d) of type %s, %s expected (ProtocolMethod.java)", name(), iArg, arg.getClass().getName(), type.getName())); + try { + for (int iArg = 0; iArg < args.length; iArg++) { + final Object arg = args[iArg]; + final Class type = this.args[iArg]; + if (!ReflectionUtil.isInstance(arg, type)) { + //throw new InternalError(String.format("Protocol method %s: illegal argument (%d) of type %s, %s expected", name(), iArg, arg.getClass().getName(), type.getName())); + System.err.println(String.format("InternalError: Protocol method %s: illegal argument (%d) of type %s, %s expected (ProtocolMethod.java)", name(), iArg, arg.getClass().getName(), type.getName())); + } } + } catch (Exception e) { + e.printStackTrace(); } } diff --git a/forge-gui/src/main/java/forge/gamemodes/net/server/NetGuiGame.java b/forge-gui/src/main/java/forge/gamemodes/net/server/NetGuiGame.java index a88d5e6d960..c3788e4b3fb 100644 --- a/forge-gui/src/main/java/forge/gamemodes/net/server/NetGuiGame.java +++ b/forge-gui/src/main/java/forge/gamemodes/net/server/NetGuiGame.java @@ -76,9 +76,9 @@ public class NetGuiGame extends AbstractGuiGame { } @Override - public void showPromptMessage(final PlayerView playerView, final String message, final CardView card) { + public void showCardPromptMessage(final PlayerView playerView, final String message, final CardView card) { updateGameView(); - send(ProtocolMethod.showPromptMessage, playerView, message); + send(ProtocolMethod.showCardPromptMessage, playerView, message, card); } @Override diff --git a/forge-gui/src/main/java/forge/gamemodes/quest/QuestWinLoseController.java b/forge-gui/src/main/java/forge/gamemodes/quest/QuestWinLoseController.java index c65241db897..35707510af0 100644 --- a/forge-gui/src/main/java/forge/gamemodes/quest/QuestWinLoseController.java +++ b/forge-gui/src/main/java/forge/gamemodes/quest/QuestWinLoseController.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map.Entry; import com.google.common.collect.Lists; +import forge.gui.GuiBase; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -304,8 +305,9 @@ public class QuestWinLoseController { if (altReward > 0) { credGameplay += altReward; - sb.append(TextUtil.concatNoSpace("Alternate win condition: ", - winConditionName, "! Bonus: ", String.valueOf(altReward), " credits.\n")); + sb.append(GuiBase.getInterface().isLibgdxPort() + ? TextUtil.concatNoSpace("Alternate win condition: ", winConditionName, "! Bonus: ", String.valueOf(altReward), " credits.\n") + : TextUtil.concatNoSpace("Alternate win condition: ", winConditionName, "! Bonus: ", String.valueOf(altReward), " credits.\n")); } } // Mulligan to zero diff --git a/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java b/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java index 849f170c837..9cf1c96fb09 100644 --- a/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java +++ b/forge-gui/src/main/java/forge/gui/interfaces/IGuiGame.java @@ -39,7 +39,7 @@ public interface IGuiGame { void afterGameEnd(); void showCombat(); void showPromptMessage(PlayerView playerView, String message); - void showPromptMessage(PlayerView playerView, String message, CardView card); + void showCardPromptMessage(PlayerView playerView, String message, CardView card); void updateButtons(PlayerView owner, boolean okEnabled, boolean cancelEnabled, boolean focusOk); void updateButtons(PlayerView owner, String label1, String label2, boolean enable1, boolean enable2, boolean focus1); void flashIncorrectAction(); diff --git a/forge-gui/src/main/java/forge/player/TargetSelection.java b/forge-gui/src/main/java/forge/player/TargetSelection.java index ba59f027510..72c9ebc6180 100644 --- a/forge-gui/src/main/java/forge/player/TargetSelection.java +++ b/forge-gui/src/main/java/forge/player/TargetSelection.java @@ -87,7 +87,7 @@ public class TargetSelection { final int maxTargets = numTargets != null ? numTargets.intValue() : ability.getMaxTargets(); //final int maxTotalCMC = tgt.getMaxTotalCMC(ability.getHostCard(), ability); final int numTargeted = ability.getTargets().size(); - final boolean isSingleZone = ability.getTargetRestrictions().isSingleZone(); + final boolean isSingleZone = getTgt().isSingleZone(); final boolean hasEnoughTargets = minTargets == 0 || numTargeted >= minTargets; final boolean hasAllTargets = numTargeted == maxTargets && maxTargets > 0; @@ -219,7 +219,7 @@ public class TargetSelection { for (final Card inZone : choices) { Zone zz = game.getZoneOf(inZone); CardView cardView = CardView.get(inZone); - if (this.ability.getTargetRestrictions() != null && this.ability.getTargetRestrictions().isWithSameCreatureType()) { + if (getTgt() != null && getTgt().isWithSameCreatureType()) { Card firstTgt = this.ability.getTargetCard(); if (firstTgt != null && !firstTgt.sharesCreatureTypeWith(inZone)) { continue;